diff --git a/app/MindWork AI Studio/Agents/AgentBase.cs b/app/MindWork AI Studio/Agents/AgentBase.cs index 3aab6c4ed..0df81ca9d 100644 --- a/app/MindWork AI Studio/Agents/AgentBase.cs +++ b/app/MindWork AI Studio/Agents/AgentBase.cs @@ -19,7 +19,7 @@ public abstract class AgentBase(ILogger logger, SettingsManager setti Time = DateTimeOffset.UtcNow, }; - protected static readonly JsonSerializerOptions JSON_SERIALIZER_OPTIONS = new() + internal static readonly JsonSerializerOptions JSON_SERIALIZER_OPTIONS = new() { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, }; diff --git a/app/MindWork AI Studio/Assistants/Builder/LauncherTextsResponse.cs b/app/MindWork AI Studio/Assistants/Builder/LauncherTextsResponse.cs index 2a79eb336..388afae1b 100644 --- a/app/MindWork AI Studio/Assistants/Builder/LauncherTextsResponse.cs +++ b/app/MindWork AI Studio/Assistants/Builder/LauncherTextsResponse.cs @@ -14,7 +14,7 @@ internal sealed class LauncherTextsResponse { public const string SCHEMA_VERSION_VALUE = "assistant_builder_launcher_texts_v1"; - private static readonly JsonSerializerOptions JSON_OPTIONS = new() + internal static readonly JsonSerializerOptions JSON_OPTIONS = new() { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, AllowTrailingCommas = false, diff --git a/app/MindWork AI Studio/Assistants/Builder/LuaResponse.Parse.cs b/app/MindWork AI Studio/Assistants/Builder/LuaResponse.Parse.cs index 5300c2c11..db33a2bcd 100644 --- a/app/MindWork AI Studio/Assistants/Builder/LuaResponse.Parse.cs +++ b/app/MindWork AI Studio/Assistants/Builder/LuaResponse.Parse.cs @@ -4,7 +4,7 @@ namespace AIStudio.Assistants.Builder; internal sealed partial class LuaResponse { - private static readonly JsonSerializerOptions JSON_OPTIONS = new() + internal static readonly JsonSerializerOptions JSON_OPTIONS = new() { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, AllowTrailingCommas = false, diff --git a/app/MindWork AI Studio/Assistants/ERI/ERIServerFileList.cs b/app/MindWork AI Studio/Assistants/ERI/ERIServerFileList.cs index 4228379be..7f9655003 100644 --- a/app/MindWork AI Studio/Assistants/ERI/ERIServerFileList.cs +++ b/app/MindWork AI Studio/Assistants/ERI/ERIServerFileList.cs @@ -13,7 +13,7 @@ namespace AIStudio.Assistants.ERI; /// internal sealed class ERIServerFileList { - private static readonly JsonSerializerOptions JSON_OPTIONS = new() + internal static readonly JsonSerializerOptions JSON_OPTIONS = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, AllowTrailingCommas = false, diff --git a/app/MindWork AI Studio/Assistants/PromptOptimizer/AssistantPromptOptimizer.razor.cs b/app/MindWork AI Studio/Assistants/PromptOptimizer/AssistantPromptOptimizer.razor.cs index a92bf737e..88a67530d 100644 --- a/app/MindWork AI Studio/Assistants/PromptOptimizer/AssistantPromptOptimizer.razor.cs +++ b/app/MindWork AI Studio/Assistants/PromptOptimizer/AssistantPromptOptimizer.razor.cs @@ -17,7 +17,7 @@ namespace AIStudio.Assistants.PromptOptimizer; public partial class AssistantPromptOptimizer : AssistantBaseCore { - private static readonly JsonSerializerOptions JSON_OPTIONS = new() + internal static readonly JsonSerializerOptions JSON_OPTIONS = new() { PropertyNameCaseInsensitive = true, }; diff --git a/app/MindWork AI Studio/Assistants/SWOTAnalysis/SwotModelAnswer.cs b/app/MindWork AI Studio/Assistants/SWOTAnalysis/SwotModelAnswer.cs index e6b20b57e..0eae03246 100644 --- a/app/MindWork AI Studio/Assistants/SWOTAnalysis/SwotModelAnswer.cs +++ b/app/MindWork AI Studio/Assistants/SWOTAnalysis/SwotModelAnswer.cs @@ -8,7 +8,7 @@ namespace AIStudio.Assistants.SWOTAnalysis; /// internal static class SwotModelAnswer { - private static readonly JsonSerializerOptions JSON_OPTIONS = new() + internal static readonly JsonSerializerOptions JSON_OPTIONS = new() { PropertyNameCaseInsensitive = true, AllowTrailingCommas = true, diff --git a/app/MindWork AI Studio/Tools/PowerPoint/SlideDeckSpec.Parsing.cs b/app/MindWork AI Studio/Tools/PowerPoint/SlideDeckSpec.Parsing.cs index 132725dd4..e4e4a786e 100644 --- a/app/MindWork AI Studio/Tools/PowerPoint/SlideDeckSpec.Parsing.cs +++ b/app/MindWork AI Studio/Tools/PowerPoint/SlideDeckSpec.Parsing.cs @@ -7,7 +7,7 @@ namespace AIStudio.Tools.PowerPoint; internal sealed partial record SlideDeckSpec { - private static readonly JsonSerializerOptions JSON_OPTIONS = new() + internal static readonly JsonSerializerOptions JSON_OPTIONS = new() { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, PropertyNameCaseInsensitive = true, @@ -158,17 +158,18 @@ internal sealed partial record SlideDeckSpec private static string Clean(string? text) => text?.Trim() ?? string.Empty; // - // What the model writes, before anything is checked. Every value may be missing or null. + // What the model writes, before anything is checked. Every value may be missing or null. Internal + // rather than private, so that the JSON context of the deck can name them: // - private sealed class DeckDto + internal sealed class DeckDto { public string? Title { get; set; } public List? Slides { get; set; } } - private sealed class SlideDto + internal sealed class SlideDto { public int? TemplateSlide { get; set; } @@ -191,7 +192,7 @@ internal sealed partial record SlideDeckSpec public JsonElement? Remove { get; set; } } - private sealed class TableDto + internal sealed class TableDto { public List? Headers { get; set; } diff --git a/app/Tests/Serialization/Fixtures/visual-briefing-build.json b/app/Tests/Serialization/Fixtures/visual-briefing-build.json new file mode 100644 index 000000000..ce7893811 --- /dev/null +++ b/app/Tests/Serialization/Fixtures/visual-briefing-build.json @@ -0,0 +1,100 @@ +{ + "buildVersion": 41, + "buildId": "00000000-0000-4000-8000-000000000002", + "operationId": "00000000-0000-4000-8000-000000000003", + "briefingId": "00000000-0000-4000-8000-000000000004", + "mode": "CHANGE_DESIGN", + "parentRevisionId": "00000000-0000-4000-8000-000000000005", + "instruction": "instruction sample 6", + "status": "COMPLETED", + "stages": [ + { + "stage": "EVIDENCE", + "status": "RUNNING", + "inputFingerprint": "inputFingerprint sample 7", + "startedAtUtc": "2026-10-29T12:38:15+01:00", + "finishedAtUtc": "2026-10-29T12:39:15+01:00", + "attempts": 50, + "outputHash": "outputHash sample 11", + "failure": { + "code": "PROVIDER_NOT_SELECTED", + "stage": "EVIDENCE", + "userMessage": "userMessage sample 12", + "technicalDetails": "technicalDetails sample 13", + "validationRule": "JSON_INVALID", + "structuredResponse": { + "issueKind": "EMPTY_RESPONSE", + "envelope": "MARKDOWN_JSON_BLOCK", + "candidateIndex": 54, + "candidateCount": 55, + "jsonPath": "/samples/jsonPath-16.txt", + "lineNumber": 4000000017, + "bytePositionInLine": 4000000018, + "fieldName": "fieldName sample 19", + "expected": "expected sample 20" + } + } + }, + { + "stage": "EVIDENCE", + "status": "RUNNING", + "inputFingerprint": "inputFingerprint sample 21", + "startedAtUtc": "2026-10-29T12:52:15+01:00", + "finishedAtUtc": "2026-10-29T12:53:15+01:00", + "attempts": 64, + "outputHash": "outputHash sample 25", + "failure": { + "code": "PROVIDER_NOT_SELECTED", + "stage": "EVIDENCE", + "userMessage": "userMessage sample 26", + "technicalDetails": "technicalDetails sample 27", + "validationRule": "JSON_INVALID", + "structuredResponse": { + "issueKind": "EMPTY_RESPONSE", + "envelope": "MARKDOWN_JSON_BLOCK", + "candidateIndex": 68, + "candidateCount": 69, + "jsonPath": "/samples/jsonPath-30.txt", + "lineNumber": 4000000031, + "bytePositionInLine": 4000000032, + "fieldName": "fieldName sample 33", + "expected": "expected sample 34" + } + } + } + ], + "contentArtifactId": "00000000-0000-4000-8000-000000000035", + "evidenceArtifactId": "00000000-0000-4000-8000-000000000036", + "planArtifactId": "00000000-0000-4000-8000-000000000037", + "presentationArtifactId": "00000000-0000-4000-8000-000000000038", + "revisionId": "00000000-0000-4000-8000-000000000039", + "committedRevisionId": "00000000-0000-4000-8000-000000000040", + "inputFingerprint": "inputFingerprint sample 41", + "sourceFingerprint": "sourceFingerprint sample 42", + "contentContractVersion": 83, + "evidenceContractVersion": 84, + "planContractVersion": 85, + "designContractVersion": 86, + "providerFamily": "providerFamily sample 47", + "model": "model sample 48", + "createdAtUtc": "2026-10-29T13:19:15+01:00", + "updatedAtUtc": "2026-10-29T13:20:15+01:00", + "failure": { + "code": "PROVIDER_NOT_SELECTED", + "stage": "EVIDENCE", + "userMessage": "userMessage sample 51", + "technicalDetails": "technicalDetails sample 52", + "validationRule": "JSON_INVALID", + "structuredResponse": { + "issueKind": "EMPTY_RESPONSE", + "envelope": "MARKDOWN_JSON_BLOCK", + "candidateIndex": 43, + "candidateCount": 44, + "jsonPath": "/samples/jsonPath-55.txt", + "lineNumber": 4000000056, + "bytePositionInLine": 4000000057, + "fieldName": "fieldName sample 58", + "expected": "expected sample 59" + } + } +} \ No newline at end of file diff --git a/app/Tests/Serialization/Fixtures/visual-briefing-compiled-data.json b/app/Tests/Serialization/Fixtures/visual-briefing-compiled-data.json new file mode 100644 index 000000000..26612dc9a --- /dev/null +++ b/app/Tests/Serialization/Fixtures/visual-briefing-compiled-data.json @@ -0,0 +1 @@ +{"slots":{"overview-title":"Text of overview-title, with Gr\u00F6\u00DFe and \u20AC","overview-summary":"Text of overview-summary, with Gr\u00F6\u00DFe and \u20AC","chart-line-title":"Text of chart-line-title, with Gr\u00F6\u00DFe and \u20AC","chart-line-caption":"Text of chart-line-caption, with Gr\u00F6\u00DFe and \u20AC","chart-area-title":"Text of chart-area-title, with Gr\u00F6\u00DFe and \u20AC","chart-area-caption":"Text of chart-area-caption, with Gr\u00F6\u00DFe and \u20AC","chart-bar-title":"Text of chart-bar-title, with Gr\u00F6\u00DFe and \u20AC","chart-bar-caption":"Text of chart-bar-caption, with Gr\u00F6\u00DFe and \u20AC","chart-stacked_bar-title":"Text of chart-stacked_bar-title, with Gr\u00F6\u00DFe and \u20AC","chart-stacked_bar-caption":"Text of chart-stacked_bar-caption, with Gr\u00F6\u00DFe and \u20AC","chart-scatter-title":"Text of chart-scatter-title, with Gr\u00F6\u00DFe and \u20AC","chart-scatter-caption":"Text of chart-scatter-caption, with Gr\u00F6\u00DFe and \u20AC","chart-pie-title":"Text of chart-pie-title, with Gr\u00F6\u00DFe and \u20AC","chart-pie-caption":"Text of chart-pie-caption, with Gr\u00F6\u00DFe and \u20AC","chart-donut-title":"Text of chart-donut-title, with Gr\u00F6\u00DFe and \u20AC","chart-donut-caption":"Text of chart-donut-caption, with Gr\u00F6\u00DFe and \u20AC","chart-radar-title":"Text of chart-radar-title, with Gr\u00F6\u00DFe and \u20AC","chart-radar-caption":"Text of chart-radar-caption, with Gr\u00F6\u00DFe and \u20AC","simulation-title":"Text of simulation-title, with Gr\u00F6\u00DFe and \u20AC","simulation-summary":"Text of simulation-summary, with Gr\u00F6\u00DFe and \u20AC","simulation-result":"Text of simulation-result, with Gr\u00F6\u00DFe and \u20AC"},"charts":{"chart-line":{"color":["#236A50","#F2D264","#79AE90","#C97857","#4E7894","#9B6B8F"],"backgroundColor":"transparent","textStyle":{"color":"#172A24","fontFamily":"system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif"},"tooltip":{"trigger":"axis","borderColor":"#D6E2DC","backgroundColor":"#FFFEFA","textStyle":{"color":"#172A24"}},"legend":{"show":true,"top":0,"textStyle":{"color":"#4F635B"}},"grid":{"left":8,"right":16,"top":48,"bottom":8,"containLabel":true},"xAxis":{"type":"category","data":["Q1","Q2","Q3"],"axisLine":{"lineStyle":{"color":"#B8C9C0"}},"axisTick":{"show":false},"axisLabel":{"color":"#5E7169"}},"yAxis":{"type":"value","axisLine":{"show":false},"axisTick":{"show":false},"axisLabel":{"color":"#5E7169"},"splitLine":{"lineStyle":{"color":"#E1EAE5"}}},"radar":null,"series":[{"name":"Revenue","type":"line","stack":null,"areaStyle":null,"smooth":true,"showSymbol":false,"symbolSize":6,"itemStyle":null,"data":[12.5,14,9.75]},{"name":"Costs","type":"line","stack":null,"areaStyle":null,"smooth":true,"showSymbol":false,"symbolSize":6,"itemStyle":null,"data":[8,7.25,10]}]},"chart-area":{"color":["#236A50","#F2D264","#79AE90","#C97857","#4E7894","#9B6B8F"],"backgroundColor":"transparent","textStyle":{"color":"#172A24","fontFamily":"system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif"},"tooltip":{"trigger":"axis","borderColor":"#D6E2DC","backgroundColor":"#FFFEFA","textStyle":{"color":"#172A24"}},"legend":{"show":true,"top":0,"textStyle":{"color":"#4F635B"}},"grid":{"left":8,"right":16,"top":48,"bottom":8,"containLabel":true},"xAxis":{"type":"category","data":["Q1","Q2","Q3"],"axisLine":{"lineStyle":{"color":"#B8C9C0"}},"axisTick":{"show":false},"axisLabel":{"color":"#5E7169"}},"yAxis":{"type":"value","axisLine":{"show":false},"axisTick":{"show":false},"axisLabel":{"color":"#5E7169"},"splitLine":{"lineStyle":{"color":"#E1EAE5"}}},"radar":null,"series":[{"name":"Revenue","type":"line","stack":null,"areaStyle":{"opacity":0.18},"smooth":true,"showSymbol":false,"symbolSize":6,"itemStyle":null,"data":[12.5,14,9.75]},{"name":"Costs","type":"line","stack":null,"areaStyle":{"opacity":0.18},"smooth":true,"showSymbol":false,"symbolSize":6,"itemStyle":null,"data":[8,7.25,10]}]},"chart-bar":{"color":["#236A50","#F2D264","#79AE90","#C97857","#4E7894","#9B6B8F"],"backgroundColor":"transparent","textStyle":{"color":"#172A24","fontFamily":"system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif"},"tooltip":{"trigger":"axis","borderColor":"#D6E2DC","backgroundColor":"#FFFEFA","textStyle":{"color":"#172A24"}},"legend":{"show":true,"top":0,"textStyle":{"color":"#4F635B"}},"grid":{"left":8,"right":16,"top":48,"bottom":8,"containLabel":true},"xAxis":{"type":"category","data":["Q1","Q2","Q3"],"axisLine":{"lineStyle":{"color":"#B8C9C0"}},"axisTick":{"show":false},"axisLabel":{"color":"#5E7169"}},"yAxis":{"type":"value","axisLine":{"show":false},"axisTick":{"show":false},"axisLabel":{"color":"#5E7169"},"splitLine":{"lineStyle":{"color":"#E1EAE5"}}},"radar":null,"series":[{"name":"Revenue","type":"bar","stack":null,"areaStyle":null,"smooth":false,"showSymbol":false,"symbolSize":6,"itemStyle":{"borderRadius":[6,6,0,0]},"data":[12.5,14,9.75]},{"name":"Costs","type":"bar","stack":null,"areaStyle":null,"smooth":false,"showSymbol":false,"symbolSize":6,"itemStyle":{"borderRadius":[6,6,0,0]},"data":[8,7.25,10]}]},"chart-stacked_bar":{"color":["#236A50","#F2D264","#79AE90","#C97857","#4E7894","#9B6B8F"],"backgroundColor":"transparent","textStyle":{"color":"#172A24","fontFamily":"system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif"},"tooltip":{"trigger":"axis","borderColor":"#D6E2DC","backgroundColor":"#FFFEFA","textStyle":{"color":"#172A24"}},"legend":{"show":true,"top":0,"textStyle":{"color":"#4F635B"}},"grid":{"left":8,"right":16,"top":48,"bottom":8,"containLabel":true},"xAxis":{"type":"category","data":["Q1","Q2","Q3"],"axisLine":{"lineStyle":{"color":"#B8C9C0"}},"axisTick":{"show":false},"axisLabel":{"color":"#5E7169"}},"yAxis":{"type":"value","axisLine":{"show":false},"axisTick":{"show":false},"axisLabel":{"color":"#5E7169"},"splitLine":{"lineStyle":{"color":"#E1EAE5"}}},"radar":null,"series":[{"name":"Revenue","type":"bar","stack":"total","areaStyle":null,"smooth":false,"showSymbol":false,"symbolSize":6,"itemStyle":{"borderRadius":[6,6,0,0]},"data":[12.5,14,9.75]},{"name":"Costs","type":"bar","stack":"total","areaStyle":null,"smooth":false,"showSymbol":false,"symbolSize":6,"itemStyle":{"borderRadius":[6,6,0,0]},"data":[8,7.25,10]}]},"chart-scatter":{"color":["#236A50","#F2D264","#79AE90","#C97857","#4E7894","#9B6B8F"],"backgroundColor":"transparent","textStyle":{"color":"#172A24","fontFamily":"system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif"},"tooltip":{"trigger":"axis","borderColor":"#D6E2DC","backgroundColor":"#FFFEFA","textStyle":{"color":"#172A24"}},"legend":{"show":true,"top":0,"textStyle":{"color":"#4F635B"}},"grid":{"left":8,"right":16,"top":48,"bottom":8,"containLabel":true},"xAxis":{"type":"category","data":["Q1","Q2","Q3"],"axisLine":{"lineStyle":{"color":"#B8C9C0"}},"axisTick":{"show":false},"axisLabel":{"color":"#5E7169"}},"yAxis":{"type":"value","axisLine":{"show":false},"axisTick":{"show":false},"axisLabel":{"color":"#5E7169"},"splitLine":{"lineStyle":{"color":"#E1EAE5"}}},"radar":null,"series":[{"name":"Revenue","type":"scatter","stack":null,"areaStyle":null,"smooth":false,"showSymbol":true,"symbolSize":10,"itemStyle":null,"data":[12.5,14,9.75]},{"name":"Costs","type":"scatter","stack":null,"areaStyle":null,"smooth":false,"showSymbol":true,"symbolSize":10,"itemStyle":null,"data":[8,7.25,10]}]},"chart-pie":{"color":["#236A50","#F2D264","#79AE90","#C97857","#4E7894","#9B6B8F"],"backgroundColor":"transparent","textStyle":{"color":"#172A24","fontFamily":"system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif"},"tooltip":{"trigger":"item","borderColor":"#D6E2DC","backgroundColor":"#FFFEFA","textStyle":{"color":"#172A24"}},"legend":{"show":true,"top":0,"textStyle":{"color":"#4F635B"}},"grid":null,"xAxis":null,"yAxis":null,"radar":null,"series":[{"type":"pie","radius":["0%","70%"],"padAngle":2,"itemStyle":{"borderColor":"#FFFEFA","borderWidth":2,"borderRadius":5},"label":{"color":"#4F635B"},"data":[{"name":"Q1","value":12.5},{"name":"Q2","value":14},{"name":"Q3","value":9.75}]}]},"chart-donut":{"color":["#236A50","#F2D264","#79AE90","#C97857","#4E7894","#9B6B8F"],"backgroundColor":"transparent","textStyle":{"color":"#172A24","fontFamily":"system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif"},"tooltip":{"trigger":"item","borderColor":"#D6E2DC","backgroundColor":"#FFFEFA","textStyle":{"color":"#172A24"}},"legend":{"show":true,"top":0,"textStyle":{"color":"#4F635B"}},"grid":null,"xAxis":null,"yAxis":null,"radar":null,"series":[{"type":"pie","radius":["45%","70%"],"padAngle":2,"itemStyle":{"borderColor":"#FFFEFA","borderWidth":2,"borderRadius":5},"label":{"color":"#4F635B"},"data":[{"name":"Q1","value":12.5},{"name":"Q2","value":14},{"name":"Q3","value":9.75}]}]},"chart-radar":{"color":["#236A50","#F2D264","#79AE90","#C97857","#4E7894","#9B6B8F"],"backgroundColor":"transparent","textStyle":{"color":"#172A24","fontFamily":"system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif"},"tooltip":{"trigger":"axis","borderColor":"#D6E2DC","backgroundColor":"#FFFEFA","textStyle":{"color":"#172A24"}},"legend":{"show":true,"top":0,"textStyle":{"color":"#4F635B"}},"grid":null,"xAxis":null,"yAxis":null,"radar":{"indicator":[{"name":"Q1"},{"name":"Q2"},{"name":"Q3"}],"splitArea":{"areaStyle":{"color":["#FFFEFA","#EAF1EC"]}},"axisName":{"color":"#5E7169"},"splitLine":{"lineStyle":{"color":"#B8C9C0"}}},"series":[{"name":"Revenue","type":"radar","data":[{"value":[12.5,14,9.75],"name":"Revenue"}]},{"name":"Costs","type":"radar","data":[{"value":[8,7.25,10],"name":"Costs"}]}]}},"interactions":{"controls":[{"controlId":"growth","componentId":"simulation","kind":"RANGE","initialValue":3,"options":[]},{"controlId":"scenario","componentId":"simulation","kind":"SELECT","initialValue":"base","options":[{"value":"base","label":"Base case"},{"value":"high","label":"Gr\u00F6\u00DFeres Wachstum"}]}],"state":{"growth":3,"scenario":"base"},"formulas":{"simulation-result":{"formulaVersion":1,"op":"multiply","args":[{"path":"interactions.state.growth"},{"value":2}]}}},"accessibility":{"chart-line":"Revenue rises in Q2 and falls in Q3.","simulation":"The growth changes the result."},"sourceReferences":{"chart-line":["S1","S2"]},"labels":{"reset":"Zur\u00FCcksetzen","brand":"MindWork AI Studio"}} \ No newline at end of file diff --git a/app/Tests/Serialization/Fixtures/visual-briefing-content-artifact.json b/app/Tests/Serialization/Fixtures/visual-briefing-content-artifact.json new file mode 100644 index 000000000..8458375b9 --- /dev/null +++ b/app/Tests/Serialization/Fixtures/visual-briefing-content-artifact.json @@ -0,0 +1,188 @@ +{ + "artifactVersion": 41, + "contractVersion": 42, + "artifactId": "00000000-0000-4000-8000-000000000003", + "createdAtUtc": "2026-10-29T12:34:15+01:00", + "payloadHash": "payloadHash sample 5", + "data": { + "sample": 6, + "text": "Gr\u00F6\u00DFe" + }, + "slots": [ + { + "slotId": "slotId sample 7", + "value": { + "sample": 8, + "text": "Gr\u00F6\u00DFe" + } + }, + { + "slotId": "slotId sample 9", + "value": { + "sample": 10, + "text": "Gr\u00F6\u00DFe" + } + } + ], + "charts": [ + { + "componentId": "componentId sample 11", + "kind": "AREA", + "categories": [ + "categories sample 12", + "categories sample 13" + ], + "series": [ + { + "name": "name sample 14", + "values": [ + 3.75, + 4.75 + ] + }, + { + "name": "name sample 15", + "values": [ + 3.75, + 4.75 + ] + } + ] + }, + { + "componentId": "componentId sample 16", + "kind": "AREA", + "categories": [ + "categories sample 17", + "categories sample 18" + ], + "series": [ + { + "name": "name sample 19", + "values": [ + 3.75, + 4.75 + ] + }, + { + "name": "name sample 20", + "values": [ + 3.75, + 4.75 + ] + } + ] + } + ], + "controls": [ + { + "controlId": "controlId sample 21", + "componentId": "componentId sample 22", + "kind": "FILTER", + "initialValue": { + "sample": 23, + "text": "Gr\u00F6\u00DFe" + }, + "options": [ + { + "value": "value sample 24", + "label": "label sample 25" + }, + { + "value": "value sample 26", + "label": "label sample 27" + } + ] + }, + { + "controlId": "controlId sample 28", + "componentId": "componentId sample 29", + "kind": "FILTER", + "initialValue": { + "sample": 30, + "text": "Gr\u00F6\u00DFe" + }, + "options": [ + { + "value": "value sample 31", + "label": "label sample 32" + }, + { + "value": "value sample 33", + "label": "label sample 34" + } + ] + } + ], + "formulas": [ + { + "componentId": "componentId sample 35", + "outputSlotId": "outputSlotId sample 36", + "formula": { + "formulaVersion": 77, + "op": "op sample 38", + "path": "/samples/path-39.txt", + "value": { + "sample": 40, + "text": "Gr\u00F6\u00DFe" + }, + "args": [] + } + }, + { + "componentId": "componentId sample 41", + "outputSlotId": "outputSlotId sample 42", + "formula": { + "formulaVersion": 83, + "op": "op sample 44", + "path": "/samples/path-45.txt", + "value": { + "sample": 46, + "text": "Gr\u00F6\u00DFe" + }, + "args": [] + } + } + ], + "accessibilityTexts": { + "accessibilityTextsKey sample 47": "accessibilityTexts sample 48", + "accessibilityTextsKey sample 49": "accessibilityTexts sample 50" + }, + "sourceReferences": { + "sourceReferencesKey sample 51": [ + "sourceReferences sample 52", + "sourceReferences sample 53" + ], + "sourceReferencesKey sample 54": [ + "sourceReferences sample 55", + "sourceReferences sample 56" + ] + }, + "resetLabel": "resetLabel sample 57", + "sourceCoverage": [ + { + "sourceId": "sourceId sample 58", + "coverage": "CONTEXTUAL", + "reason": "reason sample 59" + }, + { + "sourceId": "sourceId sample 60", + "coverage": "CONTEXTUAL", + "reason": "reason sample 61" + } + ], + "assetPlan": [ + { + "assetId": "assetId sample 62", + "description": "description sample 63", + "altText": "altText sample 64" + }, + { + "assetId": "assetId sample 65", + "description": "description sample 66", + "altText": "altText sample 67" + } + ], + "structuralSignature": "structuralSignature sample 68", + "model": "model sample 69" +} \ No newline at end of file diff --git a/app/Tests/Serialization/Fixtures/visual-briefing-evidence-artifact.json b/app/Tests/Serialization/Fixtures/visual-briefing-evidence-artifact.json new file mode 100644 index 000000000..dde3e242e --- /dev/null +++ b/app/Tests/Serialization/Fixtures/visual-briefing-evidence-artifact.json @@ -0,0 +1,142 @@ +{ + "artifactVersion": 41, + "contractVersion": 42, + "artifactId": "00000000-0000-4000-8000-000000000003", + "createdAtUtc": "2026-10-29T12:34:15+01:00", + "payloadHash": "payloadHash sample 5", + "facts": [ + { + "evidenceId": "evidenceId sample 6", + "statement": "statement sample 7", + "sourceIds": [ + "sourceIds sample 8", + "sourceIds sample 9" + ] + }, + { + "evidenceId": "evidenceId sample 10", + "statement": "statement sample 11", + "sourceIds": [ + "sourceIds sample 12", + "sourceIds sample 13" + ] + } + ], + "metrics": [ + { + "evidenceId": "evidenceId sample 14", + "label": "label sample 15", + "value": 3.75, + "unit": "unit sample 16", + "sourceIds": [ + "sourceIds sample 17", + "sourceIds sample 18" + ] + }, + { + "evidenceId": "evidenceId sample 19", + "label": "label sample 20", + "value": 3.75, + "unit": "unit sample 21", + "sourceIds": [ + "sourceIds sample 22", + "sourceIds sample 23" + ] + } + ], + "tables": [ + { + "evidenceId": "evidenceId sample 24", + "title": "title sample 25", + "columns": [ + "columns sample 26", + "columns sample 27" + ], + "rows": [ + [ + { + "sample": 28, + "text": "Gr\u00F6\u00DFe" + }, + { + "sample": 29, + "text": "Gr\u00F6\u00DFe" + } + ], + [ + { + "sample": 30, + "text": "Gr\u00F6\u00DFe" + }, + { + "sample": 31, + "text": "Gr\u00F6\u00DFe" + } + ] + ], + "sourceIds": [ + "sourceIds sample 32", + "sourceIds sample 33" + ] + }, + { + "evidenceId": "evidenceId sample 34", + "title": "title sample 35", + "columns": [ + "columns sample 36", + "columns sample 37" + ], + "rows": [ + [ + { + "sample": 38, + "text": "Gr\u00F6\u00DFe" + }, + { + "sample": 39, + "text": "Gr\u00F6\u00DFe" + } + ], + [ + { + "sample": 40, + "text": "Gr\u00F6\u00DFe" + }, + { + "sample": 41, + "text": "Gr\u00F6\u00DFe" + } + ] + ], + "sourceIds": [ + "sourceIds sample 42", + "sourceIds sample 43" + ] + } + ], + "sourceCoverage": [ + { + "sourceId": "sourceId sample 44", + "coverage": "CONTEXTUAL", + "reason": "reason sample 45" + }, + { + "sourceId": "sourceId sample 46", + "coverage": "CONTEXTUAL", + "reason": "reason sample 47" + } + ], + "assetPlan": [ + { + "assetId": "assetId sample 48", + "description": "description sample 49", + "altText": "altText sample 50" + }, + { + "assetId": "assetId sample 51", + "description": "description sample 52", + "altText": "altText sample 53" + } + ], + "model": "model sample 54" +} \ No newline at end of file diff --git a/app/Tests/Serialization/Fixtures/visual-briefing-export-manifest.json b/app/Tests/Serialization/Fixtures/visual-briefing-export-manifest.json new file mode 100644 index 000000000..d5653be72 --- /dev/null +++ b/app/Tests/Serialization/Fixtures/visual-briefing-export-manifest.json @@ -0,0 +1,23 @@ +{ + "artifactVersion": 41, + "schemaVersion": 42, + "runtimeVersion": 43, + "briefingId": "00000000-0000-4000-8000-000000000004", + "revisionId": "00000000-0000-4000-8000-000000000005", + "parentRevisionId": "00000000-0000-4000-8000-000000000006", + "name": "name sample 7", + "author": "author sample 8", + "createdAtUtc": "2026-10-29T12:39:15+01:00", + "targetLanguage": 1, + "customTargetLanguage": "customTargetLanguage sample 10", + "audienceProfile": 1, + "audienceAgeGroup": 1, + "audienceOrganizationalLevel": 1, + "audienceExpertise": 1, + "showSourceReferences": true, + "protectionLevel": "INTERNAL", + "customProtectionLevel": "customProtectionLevel sample 11", + "aiStudioVersion": "aiStudioVersion sample 12", + "runtimeAIStudioVersion": "runtimeAIStudioVersion sample 13", + "documentHash": "documentHash sample 14" +} \ No newline at end of file diff --git a/app/Tests/Serialization/Fixtures/visual-briefing-manifest.json b/app/Tests/Serialization/Fixtures/visual-briefing-manifest.json new file mode 100644 index 000000000..e03de39db --- /dev/null +++ b/app/Tests/Serialization/Fixtures/visual-briefing-manifest.json @@ -0,0 +1,137 @@ +{ + "manifestVersion": 41, + "briefingId": "00000000-0000-4000-8000-000000000002", + "name": "name sample 3", + "author": "author sample 4", + "createdAtUtc": "2026-10-29T12:35:15+01:00", + "modifiedAtUtc": "2026-10-29T12:36:15+01:00", + "settings": { + "providerId": "providerId sample 7", + "modelId": "modelId sample 8", + "profileId": "profileId sample 9", + "targetLanguage": "EN_US", + "customTargetLanguage": "customTargetLanguage sample 10", + "audienceProfile": "STUDENTS", + "audienceAgeGroup": "CHILDREN", + "audienceOrganizationalLevel": "TRAINEES", + "audienceExpertise": "NON_EXPERTS", + "showSourceReferences": true, + "optimizeImages": true, + "instruction": "instruction sample 11", + "protectionLevel": "INTERNAL", + "customProtectionLevel": "customProtectionLevel sample 12" + }, + "restrictions": { + "providerConfidence": "UNTRUSTED", + "dataSecurity": "ALLOW_ANY", + "outboundData": { + "restriction": "ONLY_LINKS_FROM_CHAT", + "origin": "MAILBOX", + "reference": "Reference sample 13" + }, + "webAddressesWrittenByModels": [ + "WebAddressesWrittenByModels sample 15", + "WebAddressesWrittenByModels sample 14" + ] + }, + "sources": [ + { + "sourceId": "00000000-0000-4000-8000-000000000016", + "kind": "VISUAL_ASSET", + "path": "/samples/path-17.txt", + "size": 4000000018, + "lastWriteTimeUtc": "2026-10-29T12:49:15+01:00", + "transcriptStatus": "CURRENT", + "isMedia": true, + "assetId": "assetId sample 20" + }, + { + "sourceId": "00000000-0000-4000-8000-000000000021", + "kind": "VISUAL_ASSET", + "path": "/samples/path-22.txt", + "size": 4000000023, + "lastWriteTimeUtc": "2026-10-29T12:54:15+01:00", + "transcriptStatus": "CURRENT", + "isMedia": true, + "assetId": "assetId sample 25" + } + ], + "versions": [ + { + "schemaVersion": 66, + "intermediateArtifactVersion": 67, + "evidenceContractVersion": 68, + "planContractVersion": 69, + "contentContractVersion": 70, + "designContractVersion": 71, + "versionNumber": 72, + "revisionId": "00000000-0000-4000-8000-000000000033", + "parentRevisionId": "00000000-0000-4000-8000-000000000034", + "createdAtUtc": "2026-10-29T13:05:15+01:00", + "editMode": "CHANGE_DESIGN", + "instruction": "instruction sample 36", + "documentHash": "documentHash sample 37", + "origin": "origin sample 38", + "fileName": "fileName sample 39", + "dataHash": "dataHash sample 40", + "assetHash": "assetHash sample 41", + "templateHash": "templateHash sample 42", + "cssHash": "cssHash sample 43", + "runtimeHash": "runtimeHash sample 44", + "contentArtifactId": "00000000-0000-4000-8000-000000000045", + "evidenceArtifactId": "00000000-0000-4000-8000-000000000046", + "planArtifactId": "00000000-0000-4000-8000-000000000047", + "presentationArtifactId": "00000000-0000-4000-8000-000000000048", + "buildId": "00000000-0000-4000-8000-000000000049", + "operationId": "00000000-0000-4000-8000-000000000050", + "modelContributions": [ + { + "role": "PLAN", + "model": "Model sample 51" + }, + { + "role": "PLAN", + "model": "Model sample 52" + } + ] + }, + { + "schemaVersion": 43, + "intermediateArtifactVersion": 44, + "evidenceContractVersion": 45, + "planContractVersion": 46, + "contentContractVersion": 47, + "designContractVersion": 48, + "versionNumber": 49, + "revisionId": "00000000-0000-4000-8000-000000000060", + "parentRevisionId": "00000000-0000-4000-8000-000000000061", + "createdAtUtc": "2026-10-29T13:32:15+01:00", + "editMode": "CHANGE_DESIGN", + "instruction": "instruction sample 63", + "documentHash": "documentHash sample 64", + "origin": "origin sample 65", + "fileName": "fileName sample 66", + "dataHash": "dataHash sample 67", + "assetHash": "assetHash sample 68", + "templateHash": "templateHash sample 69", + "cssHash": "cssHash sample 70", + "runtimeHash": "runtimeHash sample 71", + "contentArtifactId": "00000000-0000-4000-8000-000000000072", + "evidenceArtifactId": "00000000-0000-4000-8000-000000000073", + "planArtifactId": "00000000-0000-4000-8000-000000000074", + "presentationArtifactId": "00000000-0000-4000-8000-000000000075", + "buildId": "00000000-0000-4000-8000-000000000076", + "operationId": "00000000-0000-4000-8000-000000000077", + "modelContributions": [ + { + "role": "PLAN", + "model": "Model sample 78" + }, + { + "role": "PLAN", + "model": "Model sample 79" + } + ] + } + ] +} \ No newline at end of file diff --git a/app/Tests/Serialization/Fixtures/visual-briefing-plan-artifact.json b/app/Tests/Serialization/Fixtures/visual-briefing-plan-artifact.json new file mode 100644 index 000000000..15ff75bc0 --- /dev/null +++ b/app/Tests/Serialization/Fixtures/visual-briefing-plan-artifact.json @@ -0,0 +1,107 @@ +{ + "artifactVersion": 41, + "contractVersion": 42, + "artifactId": "00000000-0000-4000-8000-000000000003", + "createdAtUtc": "2026-10-29T12:34:15+01:00", + "payloadHash": "payloadHash sample 5", + "sections": [ + { + "sectionId": "sectionId sample 6", + "role": "EXECUTIVE_SUMMARY", + "titleSlotId": "titleSlotId sample 7", + "summarySlotId": "summarySlotId sample 8", + "components": [ + { + "componentId": "componentId sample 9", + "kind": "METRIC", + "evidenceIds": [ + "evidenceIds sample 10", + "evidenceIds sample 11" + ], + "slots": [ + { + "slotId": "slotId sample 12", + "role": "TITLE" + }, + { + "slotId": "slotId sample 13", + "role": "TITLE" + } + ], + "assetId": "assetId sample 14", + "timelineOrientation": "VERTICAL" + }, + { + "componentId": "componentId sample 15", + "kind": "METRIC", + "evidenceIds": [ + "evidenceIds sample 16", + "evidenceIds sample 17" + ], + "slots": [ + { + "slotId": "slotId sample 18", + "role": "TITLE" + }, + { + "slotId": "slotId sample 19", + "role": "TITLE" + } + ], + "assetId": "assetId sample 20", + "timelineOrientation": "VERTICAL" + } + ] + }, + { + "sectionId": "sectionId sample 21", + "role": "EXECUTIVE_SUMMARY", + "titleSlotId": "titleSlotId sample 22", + "summarySlotId": "summarySlotId sample 23", + "components": [ + { + "componentId": "componentId sample 24", + "kind": "METRIC", + "evidenceIds": [ + "evidenceIds sample 25", + "evidenceIds sample 26" + ], + "slots": [ + { + "slotId": "slotId sample 27", + "role": "TITLE" + }, + { + "slotId": "slotId sample 28", + "role": "TITLE" + } + ], + "assetId": "assetId sample 29", + "timelineOrientation": "VERTICAL" + }, + { + "componentId": "componentId sample 30", + "kind": "METRIC", + "evidenceIds": [ + "evidenceIds sample 31", + "evidenceIds sample 32" + ], + "slots": [ + { + "slotId": "slotId sample 33", + "role": "TITLE" + }, + { + "slotId": "slotId sample 34", + "role": "TITLE" + } + ], + "assetId": "assetId sample 35", + "timelineOrientation": "VERTICAL" + } + ] + } + ], + "structuralSignature": "structuralSignature sample 36", + "model": "model sample 37" +} \ No newline at end of file diff --git a/app/Tests/Serialization/Fixtures/visual-briefing-presentation-artifact.json b/app/Tests/Serialization/Fixtures/visual-briefing-presentation-artifact.json new file mode 100644 index 000000000..17538530e --- /dev/null +++ b/app/Tests/Serialization/Fixtures/visual-briefing-presentation-artifact.json @@ -0,0 +1,29 @@ +{ + "artifactVersion": 41, + "contractVersion": 42, + "artifactId": "00000000-0000-4000-8000-000000000003", + "createdAtUtc": "2026-10-29T12:34:15+01:00", + "payloadHash": "payloadHash sample 5", + "layout": { + "nodeId": "nodeId sample 6", + "kind": "STACK", + "sectionId": "sectionId sample 7", + "componentId": "componentId sample 8", + "children": [], + "columns": { + "mobile": 49, + "tablet": 50, + "desktop": 51 + }, + "span": 52, + "order": 53, + "emphasized": true, + "alignment": "CENTER" + }, + "profile": "EXECUTIVE", + "templateHtml": "templateHtml sample 14", + "css": "css sample 15", + "templateHash": "templateHash sample 16", + "cssHash": "cssHash sample 17", + "model": "model sample 18" +} \ No newline at end of file diff --git a/app/Tests/Serialization/JsonContractTests.cs b/app/Tests/Serialization/JsonContractTests.cs index 3f4ac8961..672e7f697 100644 --- a/app/Tests/Serialization/JsonContractTests.cs +++ b/app/Tests/Serialization/JsonContractTests.cs @@ -18,6 +18,10 @@ public sealed class JsonContractTests [TestCaseSource(typeof(JsonTestRoots), nameof(JsonTestRoots.RuntimeAnswers))] [TestCaseSource(typeof(JsonTestRoots), nameof(JsonTestRoots.ServiceRequests))] [TestCaseSource(typeof(JsonTestRoots), nameof(JsonTestRoots.ServiceAnswers))] + [TestCaseSource(typeof(JsonTestRoots), nameof(JsonTestRoots.VisualBriefingStored))] + [TestCaseSource(typeof(JsonTestRoots), nameof(JsonTestRoots.VisualBriefingHashed))] + [TestCaseSource(typeof(JsonTestRoots), nameof(JsonTestRoots.VisualBriefingAnswers))] + [TestCaseSource(typeof(JsonTestRoots), nameof(JsonTestRoots.ModelAnswers))] public void TheContractsMatchThoseOfReflection(JsonTestRoot root) { var baseline = JsonReflectionBaseline.Of(root.Options); diff --git a/app/Tests/Serialization/JsonDefaultsTests.cs b/app/Tests/Serialization/JsonDefaultsTests.cs index cbdad19a5..973ebde55 100644 --- a/app/Tests/Serialization/JsonDefaultsTests.cs +++ b/app/Tests/Serialization/JsonDefaultsTests.cs @@ -8,7 +8,7 @@ namespace AIStudio.Tests.Serialization; /// /// /// Files written by an older version of AI Studio lack every property added since, and an answer of a -/// provider, of the runtime, or of a service leaves out whatever it has nothing to say about. Reading them has +/// provider, of the runtime, of a service, or of a model leaves out whatever it has nothing to say about. Reading them has /// to give those properties their defaults, as a new object has them, e.g., a sub-object of the settings /// instead of null. Source generation in .NET 10 breaks this for init-only properties: it sets them all while /// creating the object, and a property missing in the JSON gets the default of its type instead @@ -25,6 +25,9 @@ public sealed class JsonDefaultsTests [TestCaseSource(typeof(JsonTestRoots), nameof(JsonTestRoots.ProviderAnswers))] [TestCaseSource(typeof(JsonTestRoots), nameof(JsonTestRoots.RuntimeAnswers))] [TestCaseSource(typeof(JsonTestRoots), nameof(JsonTestRoots.ServiceAnswers))] + [TestCaseSource(typeof(JsonTestRoots), nameof(JsonTestRoots.VisualBriefingStored))] + [TestCaseSource(typeof(JsonTestRoots), nameof(JsonTestRoots.VisualBriefingAnswers))] + [TestCaseSource(typeof(JsonTestRoots), nameof(JsonTestRoots.ModelAnswers))] public void AnEmptyObjectGetsTheDefaultsOfANewOne(JsonTestRoot root) { var baseline = JsonReflectionBaseline.Of(root.Options); diff --git a/app/Tests/Serialization/JsonGoldenFileTests.cs b/app/Tests/Serialization/JsonGoldenFileTests.cs index b9e8ca756..77cfc8dae 100644 --- a/app/Tests/Serialization/JsonGoldenFileTests.cs +++ b/app/Tests/Serialization/JsonGoldenFileTests.cs @@ -3,6 +3,7 @@ using System.Text.Json; using System.Text.Json.Nodes; using AIStudio.Assistants.SWOTAnalysis; +using AIStudio.Assistants.VisualBriefing; using AIStudio.Chat; using AIStudio.Settings; using AIStudio.Settings.DataModel; @@ -42,6 +43,15 @@ public sealed class JsonGoldenFileTests yield return new("data-restore-plan.json", typeof(DataRestorePlan), DataBackupFormat.JSON_OPTIONS); yield return new("data-restore-report.json", typeof(DataRestoreReport), DataBackupFormat.JSON_OPTIONS); yield return new("provenance-surrounding-content.json", typeof(List), SqliteProvenanceStoreClientImplementation.JSON_OPTIONS); + + // A visual briefing, with the artifacts of its stages, a build, and the header of an exported briefing: + yield return new("visual-briefing-manifest.json", typeof(VisualBriefingManifest), VisualBriefingJson.Persistence); + yield return new("visual-briefing-evidence-artifact.json", typeof(VisualBriefingEvidenceArtifact), VisualBriefingJson.Persistence); + yield return new("visual-briefing-plan-artifact.json", typeof(VisualBriefingPlanArtifact), VisualBriefingJson.Persistence); + yield return new("visual-briefing-content-artifact.json", typeof(VisualBriefingContentArtifact), VisualBriefingJson.Persistence); + yield return new("visual-briefing-presentation-artifact.json", typeof(VisualBriefingPresentationArtifact), VisualBriefingJson.Persistence); + yield return new("visual-briefing-build.json", typeof(VisualBriefingBuildRecord), VisualBriefingJson.Persistence); + yield return new("visual-briefing-export-manifest.json", typeof(VisualBriefingExportManifest), VisualBriefingJson.Canonical); } [TestCaseSource(nameof(GoldenFiles))] diff --git a/app/Tests/Serialization/JsonTestRoots.ModelAnswers.cs b/app/Tests/Serialization/JsonTestRoots.ModelAnswers.cs new file mode 100644 index 000000000..25b77392b --- /dev/null +++ b/app/Tests/Serialization/JsonTestRoots.ModelAnswers.cs @@ -0,0 +1,36 @@ +using AIStudio.Agents; +using AIStudio.Agents.AssistantAudit; +using AIStudio.Assistants.Builder; +using AIStudio.Assistants.ERI; +using AIStudio.Assistants.PromptOptimizer; +using AIStudio.Assistants.SWOTAnalysis; +using AIStudio.Tools.PowerPoint; + +namespace AIStudio.Tests.Serialization; + +public static partial class JsonTestRoots +{ + /// + /// The JSON which models write for the agents and assistants of AI Studio, apart from the Visual Briefing + /// assistant. + /// + public static IEnumerable ModelAnswers() + { + TestHost.PrepareStaticState(); + + // The agents, which select data sources, check retrieved passages, and audit assistant plugins: + yield return Case(new(typeof(List), AgentBase.JSON_SERIALIZER_OPTIONS)); + yield return Case(new(typeof(RetrievalContextValidationResult), AgentBase.JSON_SERIALIZER_OPTIONS)); + yield return Case(new(typeof(AssistantAuditResult), AgentBase.JSON_SERIALIZER_OPTIONS)); + + // The Assistant Builder, for assistant plugins and for direct chat launchers: + yield return Case(new(typeof(LuaResponse), LuaResponse.JSON_OPTIONS)); + yield return Case(new(typeof(LauncherTextsResponse), LauncherTextsResponse.JSON_OPTIONS)); + + yield return Case(new(typeof(PromptOptimizationResult), AssistantPromptOptimizer.JSON_OPTIONS)); + yield return Case(new(typeof(SwotCategoryAnalysisResult), SwotModelAnswer.JSON_OPTIONS)); + yield return Case(new(typeof(SwotModerationResult), SwotModelAnswer.JSON_OPTIONS)); + yield return Case(new(typeof(ERIServerFileList), ERIServerFileList.JSON_OPTIONS)); + yield return Case(new(typeof(SlideDeckSpec.DeckDto), SlideDeckSpec.JSON_OPTIONS)); + } +} \ No newline at end of file diff --git a/app/Tests/Serialization/JsonTestRoots.VisualBriefing.cs b/app/Tests/Serialization/JsonTestRoots.VisualBriefing.cs new file mode 100644 index 000000000..2b4f319ac --- /dev/null +++ b/app/Tests/Serialization/JsonTestRoots.VisualBriefing.cs @@ -0,0 +1,87 @@ +using System.Text.Json; + +using AIStudio.Assistants.VisualBriefing; + +namespace AIStudio.Tests.Serialization; + +public static partial class JsonTestRoots +{ + /// + /// What the Visual Briefing assistant writes and reads again: the files of a briefing, and the canonical + /// JSON which it parses back from an exported briefing. + /// + public static IEnumerable VisualBriefingStored() + { + TestHost.PrepareStaticState(); + var options = VisualBriefingJson.Persistence; + + yield return Case(new(typeof(VisualBriefingManifest), options)); + yield return Case(new(typeof(VisualBriefingEvidenceArtifact), options)); + yield return Case(new(typeof(VisualBriefingPlanArtifact), options)); + yield return Case(new(typeof(VisualBriefingContentArtifact), options)); + yield return Case(new(typeof(VisualBriefingPresentationArtifact), options)); + yield return Case(new(typeof(VisualBriefingBuildRecord), options)); + + // The briefing selected last, which gets written without options of its own: + yield return Case(new(typeof(Guid?), JsonSerializerOptions.Default), "selected briefing"); + + // + // The canonical JSON which gets read back: the header and the data of an exported briefing, the version + // of AI Studio inside its runtime script, and the copy of a build which the progress view shows: + // + var canonical = VisualBriefingJson.Canonical; + yield return Case(new(typeof(VisualBriefingExportManifest), canonical)); + yield return Case(new(typeof(Dictionary), canonical), "data"); + yield return Case(new(typeof(string), canonical), "version"); + yield return Case(new(typeof(VisualBriefingBuildRecord), canonical), "progress"); + } + + /// + /// What the Visual Briefing assistant writes as canonical JSON without reading it again: what goes into the + /// hashes of a briefing, into the data of a briefing, and into the prompts of its stages. + /// + public static IEnumerable VisualBriefingHashed() + { + TestHost.PrepareStaticState(); + var options = VisualBriefingJson.Canonical; + + // The payload hashes of the artifacts: + yield return Case(new(typeof(List), options)); + yield return Case(new(typeof(List), options)); + yield return Case(new(typeof(List), options)); + yield return Case(new(typeof(List), options)); + yield return Case(new(typeof(List), options)); + yield return Case(new(typeof(List), options)); + yield return Case(new(typeof(List), options)); + yield return Case(new(typeof(List), options)); + yield return Case(new(typeof(List), options)); + yield return Case(new(typeof(List), options)); + yield return Case(new(typeof(Dictionary), options)); + yield return Case(new(typeof(Dictionary>), options)); + yield return Case(new(typeof(VisualBriefingLayoutNode), options)); + + // The data of a briefing, and the IDs the prompts list: + yield return Case(new(typeof(JsonElement), options)); + yield return Case(new(typeof(string[]), options)); + + // The position inside a repeated element, which the data bindings write without options of their own: + yield return Case(new(typeof(int), JsonSerializerOptions.Default), "index"); + + // The diagnostics a user copies to the clipboard: + yield return Case(new(typeof(VisualBriefingOperationDiagnostics), VisualBriefingJson.Persistence)); + } + + /// + /// The answers of the models which build a visual briefing. + /// + public static IEnumerable VisualBriefingAnswers() + { + TestHost.PrepareStaticState(); + var options = VisualBriefingJson.Canonical; + + yield return Case(new(typeof(VisualBriefingEvidenceResponse), options)); + yield return Case(new(typeof(VisualBriefingPlanResponse), options)); + yield return Case(new(typeof(VisualBriefingContentResponse), options)); + yield return Case(new(typeof(VisualBriefingDesignResponse), options)); + } +} \ No newline at end of file diff --git a/app/Tests/Serialization/VisualBriefingHashTests.cs b/app/Tests/Serialization/VisualBriefingHashTests.cs new file mode 100644 index 000000000..95213eaa3 --- /dev/null +++ b/app/Tests/Serialization/VisualBriefingHashTests.cs @@ -0,0 +1,67 @@ +using System.Text.Json; + +using AIStudio.Assistants.VisualBriefing; + +namespace AIStudio.Tests.Serialization; + +/// +/// Checks that the Visual Briefing assistant computes the same hashes and compiles the same data as before. +/// +/// +/// A stored briefing carries the hashes of its artifacts, and an exported one the hash of its document. The +/// assistant computes them anew from the canonical JSON of the same values and discards whatever no longer +/// matches, so a byte written otherwise makes every stored briefing fail its check. The expected hashes were +/// computed from the files in the folder Fixtures with the contracts of reflection, before AI Studio got +/// contracts of its own. +/// +[TestFixture] +public sealed class VisualBriefingHashTests +{ + private static IEnumerable StoredHashes() + { + yield return Case("evidence", "aed455ba77468d47c30aba31a744df35639978c3d56fe30469b298aa34918752", () => + { + var evidence = Read("visual-briefing-evidence-artifact.json", VisualBriefingJson.Persistence); + return VisualBriefingPayloadHash.ForEvidence(evidence.Facts, evidence.Metrics, evidence.Tables, evidence.SourceCoverage, evidence.AssetPlan); + }); + + yield return Case("plan", "311c5e8c1202a0f473a9f615c3aa3bc8f1015f8a37859b7addfcab879fa23f0d", () => + { + var plan = Read("visual-briefing-plan-artifact.json", VisualBriefingJson.Persistence); + return VisualBriefingPayloadHash.ForPlan(plan.Sections, plan.StructuralSignature); + }); + + yield return Case("content", "1cc333a62da8a14d76c37b7eed0a1ece71b5cb48b914cde3fed1a55bf39950c2", () => + { + var content = Read("visual-briefing-content-artifact.json", VisualBriefingJson.Persistence); + return VisualBriefingPayloadHash.ForContent(content.Slots, content.Charts, content.Controls, content.Formulas, content.AccessibilityTexts, + content.SourceReferences, content.ResetLabel, content.SourceCoverage, content.AssetPlan, content.StructuralSignature); + }); + + yield return Case("presentation", "96fc7f856c4edfba2b823b83e5cc3b09b8939124d6c5a70d07669cf61219ac3e", () => + { + var presentation = Read("visual-briefing-presentation-artifact.json", VisualBriefingJson.Persistence); + return VisualBriefingPayloadHash.ForPresentation(presentation.Layout, presentation.Profile, presentation.TemplateHash, presentation.CssHash); + }); + + yield return Case("header of an export", "a838f56ba91facf5186ecb63e790ed9a9419ad73c2669fd8bfed97bd8c876f99", () => + { + var header = Read("visual-briefing-export-manifest.json", VisualBriefingJson.Canonical); + return VisualBriefingHashing.Compute(VisualBriefingHashing.CanonicalJson(header)); + }); + } + + [TestCaseSource(nameof(StoredHashes))] + public void AStoredBriefingGetsTheSameHash(Func computeHash, string expectedHash) => Assert.That(computeHash(), Is.EqualTo(expectedHash)); + + [Test] + public void TheDataOfABriefingGetsCompiledAsBefore() + { + var compiled = VisualBriefingLayoutCompiler.Compile(VisualBriefingSamples.Plan(), VisualBriefingSamples.Content(), VisualBriefingSamples.Layout(), VisualBriefingDesignProfile.EDITORIAL); + Assert.That(compiled.Data.GetRawText(), Is.EqualTo(JsonGoldenFileTests.ReadFixture("visual-briefing-compiled-data.json"))); + } + + private static TestCaseData Case(string name, string expectedHash, Func computeHash) => new TestCaseData(computeHash, expectedHash).SetArgDisplayNames(name); + + private static T Read(string fileName, JsonSerializerOptions options) => JsonSerializer.Deserialize(JsonGoldenFileTests.ReadFixture(fileName), options)!; +} \ No newline at end of file diff --git a/app/Tests/Serialization/VisualBriefingSamples.cs b/app/Tests/Serialization/VisualBriefingSamples.cs new file mode 100644 index 000000000..4e5ba5c21 --- /dev/null +++ b/app/Tests/Serialization/VisualBriefingSamples.cs @@ -0,0 +1,155 @@ +using System.Text.Json; + +using AIStudio.Assistants.VisualBriefing; + +namespace AIStudio.Tests.Serialization; + +/// +/// A made-up visual briefing for the tests which check the JSON the Visual Briefing assistant compiles. +/// +/// +/// The briefing holds one chart of every kind and a simulation with controls and a formula, so that every shape +/// of the compiled data occurs. Its layout holds no components: the data of a briefing comes from its content +/// alone. +/// +internal static class VisualBriefingSamples +{ + private static readonly VisualBriefingChartKind[] CHART_KINDS = Enum.GetValues(); + + internal static VisualBriefingPlanArtifact Plan() => new() + { + Sections = + [ + new() + { + SectionId = "overview", + Role = VisualBriefingSectionRole.HERO, + TitleSlotId = "overview-title", + SummarySlotId = "overview-summary", + Components = + [ + .. CHART_KINDS.Select(kind => new VisualBriefingPlanComponent + { + ComponentId = ChartId(kind), + Kind = VisualBriefingComponentKind.CHART, + EvidenceIds = ["F1"], + Slots = + [ + new() { SlotId = $"{ChartId(kind)}-title", Role = VisualBriefingSlotRole.TITLE }, + new() { SlotId = $"{ChartId(kind)}-caption", Role = VisualBriefingSlotRole.CAPTION }, + ], + }), + + new() + { + ComponentId = "simulation", + Kind = VisualBriefingComponentKind.SIMULATION, + EvidenceIds = ["M1"], + Slots = + [ + new() { SlotId = "simulation-title", Role = VisualBriefingSlotRole.TITLE }, + new() { SlotId = "simulation-summary", Role = VisualBriefingSlotRole.SUMMARY }, + new() { SlotId = "simulation-result", Role = VisualBriefingSlotRole.RESULT }, + ], + }, + ], + }, + ], + }; + + internal static VisualBriefingContentArtifact Content() + { + var plan = Plan(); + var slotIds = plan.Sections.SelectMany(section => new[] { section.TitleSlotId, section.SummarySlotId } + .Concat(section.Components.SelectMany(component => component.Slots.Select(slot => slot.SlotId)))); + + return new() + { + Slots = [.. slotIds.Select(slotId => new VisualBriefingSlotValue { SlotId = slotId, Value = Json($"\"Text of {slotId}, with Größe and €\"") })], + Charts = + [ + .. CHART_KINDS.Select(kind => new VisualBriefingChartSpec + { + ComponentId = ChartId(kind), + Kind = kind, + Categories = ["Q1", "Q2", "Q3"], + Series = + [ + new() { Name = "Revenue", Values = [12.5m, 14m, 9.75m] }, + new() { Name = "Costs", Values = [8m, 7.25m, 10m] }, + ], + }), + ], + + Controls = + [ + new() + { + ControlId = "growth", + ComponentId = "simulation", + Kind = VisualBriefingControlKind.RANGE, + InitialValue = Json("3"), + }, + + new() + { + ControlId = "scenario", + ComponentId = "simulation", + Kind = VisualBriefingControlKind.SELECT, + InitialValue = Json("\"base\""), + Options = + [ + new() { Value = "base", Label = "Base case" }, + new() { Value = "high", Label = "Größeres Wachstum" }, + ], + }, + ], + + Formulas = + [ + new() + { + ComponentId = "simulation", + OutputSlotId = "simulation-result", + Formula = new() + { + FormulaVersion = 1, + Operation = "multiply", + Arguments = + [ + new() { Path = "interactions.state.growth" }, + new() { Value = Json("2") }, + ], + }, + }, + ], + + AccessibilityTexts = new(StringComparer.Ordinal) + { + [ChartId(VisualBriefingChartKind.LINE)] = "Revenue rises in Q2 and falls in Q3.", + ["simulation"] = "The growth changes the result.", + }, + + SourceReferences = new(StringComparer.Ordinal) + { + [ChartId(VisualBriefingChartKind.LINE)] = ["S1", "S2"], + }, + + ResetLabel = "Zurücksetzen", + }; + } + + internal static VisualBriefingLayoutNode Layout() => new() + { + NodeId = "root", + Kind = VisualBriefingLayoutNodeKind.STACK, + }; + + private static string ChartId(VisualBriefingChartKind kind) => $"chart-{kind.ToString().ToLowerInvariant()}"; + + private static JsonElement Json(string json) + { + using var document = JsonDocument.Parse(json); + return document.RootElement.Clone(); + } +} \ No newline at end of file