Added tests which guard the stored JSON before source generation

This commit is contained in:
Thorsten Sommer committed 2026-11-01 20:50:42 +01:00
1 parent fc7399e81b
commit eafc134ab8
23 files changed
+4807 -3

No files matched your search

@@ -10,7 +10,7 @@ internal static class SwotAnalysisContentPresentation
/// Fails the reading of stored data which holds a null where the result expects a value, so that
/// the result view never meets one.
/// </summary>
private static readonly JsonSerializerOptions JSON_OPTIONS = new()
internal static readonly JsonSerializerOptions JSON_OPTIONS = new()
{
RespectNullableAnnotations = true,
};
@@ -39,7 +39,7 @@ public sealed class SqliteProvenanceStoreClientImplementation(string name, strin
/// Writes the content around a passage the way it reads: the default encoder would turn every
/// character outside ASCII into an escape, cf. ToolExecutionResult.MODEL_CONTENT_OPTIONS.
/// </summary>
private static readonly JsonSerializerOptions JSON_OPTIONS = new() { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping };
internal static readonly JsonSerializerOptions JSON_OPTIONS = new() { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping };
private readonly string databasePath = databasePath;
private readonly string lastCleanupPath = Path.Join(basePath, LAST_CLEANUP_FILENAME);
+442
View File
@@ -0,0 +1,442 @@
{
"chat_id": "00000000-0000-4000-8000-000000000001",
"workspace_id": "00000000-0000-4000-8000-000000000002",
"last_media_transcript_number": 9000000000,
"draft": {
"text": "text sample 3",
"file_attachments": [
{
"$type": "file",
"type": "IMAGE",
"file_name": "FileName sample 4",
"file_path": "/samples/FilePath-5.txt",
"file_size_bytes": 4000000006,
"is_forbidden": false,
"is_valid": true,
"is_image": true,
"file_path_as_url": "/local/file?path=%2Fsamples%2FFilePath-5.txt",
"provenance_id": "00000000-0000-4000-8000-000000000007",
"version": {
"last_write_utc": "2026-10-29T11:08:00Z",
"length": 4000000009
}
},
{
"$type": "image",
"source_type": "LOCAL_PATH",
"type": "IMAGE",
"file_name": "FileName sample 10",
"file_path": "/samples/FilePath-11.txt",
"file_size_bytes": 4000000012,
"is_forbidden": false,
"is_valid": true,
"is_image": true,
"file_path_as_url": "/local/file?path=%2Fsamples%2FFilePath-11.txt",
"provenance_id": "00000000-0000-4000-8000-000000000013",
"version": {
"last_write_utc": "2026-10-29T11:14:00Z",
"length": 4000000015
}
},
{
"$type": "managed_transcript",
"original_file_name": "OriginalFileName sample 19",
"is_staged": true,
"type": "IMAGE",
"file_name": "FileName sample 16",
"file_path": "/samples/FilePath-17.txt",
"file_size_bytes": 4000000018,
"is_forbidden": false,
"is_valid": true,
"is_image": false,
"file_path_as_url": "/local/file?path=%2Fsamples%2FFilePath-17.txt",
"provenance_id": "00000000-0000-4000-8000-000000000020",
"version": {
"last_write_utc": "2026-10-29T11:21:00Z",
"length": 4000000022
}
},
{
"$type": "managed_transcript",
"original_file_name": "OriginalFileName sample 34",
"is_staged": true,
"type": "IMAGE",
"file_name": "FileName sample 31",
"file_path": "/samples/FilePath-32.txt",
"file_size_bytes": 4000000033,
"is_forbidden": false,
"is_valid": true,
"is_image": false,
"file_path_as_url": "/local/file?path=%2Fsamples%2FFilePath-32.txt",
"provenance_id": "00000000-0000-4000-8000-000000000035",
"version": {
"last_write_utc": "2026-10-29T11:36:00Z",
"length": 4000000037
}
},
{
"$type": "managed_transcript",
"original_file_name": "OriginalFileName sample 41",
"is_staged": true,
"type": "IMAGE",
"file_name": "FileName sample 38",
"file_path": "/samples/FilePath-39.txt",
"file_size_bytes": 4000000040,
"is_forbidden": false,
"is_valid": true,
"is_image": false,
"file_path_as_url": "/local/file?path=%2Fsamples%2FFilePath-39.txt",
"provenance_id": "00000000-0000-4000-8000-000000000042",
"version": {
"last_write_utc": "2026-10-29T11:43:00Z",
"length": 4000000044
}
}
],
"previewed_texts": [
{
"file_path": "/samples/FilePath-23.txt",
"provenance_id": "00000000-0000-4000-8000-000000000024",
"version": {
"last_write_utc": "2026-10-29T11:25:00Z",
"length": 4000000026
}
},
{
"file_path": "/samples/FilePath-27.txt",
"provenance_id": "00000000-0000-4000-8000-000000000028",
"version": {
"last_write_utc": "2026-10-29T11:29:00Z",
"length": 4000000030
}
}
]
},
"selected_provider": "selected_provider sample 45",
"selected_profile": "selected_profile sample 46",
"selected_chat_template": "selected_chat_template sample 47",
"selected_tool_ids": [
"selected_tool_ids sample 48",
"selected_tool_ids sample 49"
],
"include_date_time": true,
"data_source_options": {
"disable_data_sources": true,
"automatic_data_source_selection": true,
"automatic_validation": true,
"retrieval_mode": "EVERY_MESSAGE",
"preselected_data_source_ids": [
"preselected_data_source_ids sample 50",
"preselected_data_source_ids sample 51"
]
},
"ai_selected_data_sources": [
{
"data_source": {
"$type_discriminator": "LOCAL_DIRECTORY",
"num": 7,
"id": "id sample 52",
"name": "name sample 53",
"description": "description sample 54",
"type": "LOCAL_FILE",
"embedding_id": "embedding_id sample 55",
"max_chunk_token_length": 46,
"chunk_overlap_token_length": 47,
"confidence_level": "UNTRUSTED",
"is_enterprise_configuration": true,
"enterprise_configuration_plugin_id": "00000000-0000-4000-8000-000000000058",
"max_matches": 8,
"outbound_data_restriction": "ONLY_LINKS_FROM_CHAT",
"path": "/samples/path-59.txt",
"file_patterns": "file_patterns sample 60",
"exclude_patterns": "exclude_patterns sample 61",
"max_file_size_megabytes": 52,
"include_subdirectories": true,
"include_hidden_files_and_folders": true
},
"ai_decision": {
"id": "id sample 63",
"reason": "reason sample 64",
"confidence": 2.25
},
"selected": true
},
{
"data_source": {
"$type_discriminator": "LOCAL_FILE",
"num": 7,
"id": "id sample 65",
"name": "name sample 66",
"description": "description sample 67",
"type": "LOCAL_FILE",
"embedding_id": "embedding_id sample 68",
"max_chunk_token_length": 59,
"chunk_overlap_token_length": 60,
"confidence_level": "UNTRUSTED",
"is_enterprise_configuration": true,
"enterprise_configuration_plugin_id": "00000000-0000-4000-8000-000000000071",
"max_matches": 8,
"outbound_data_restriction": "ONLY_LINKS_FROM_CHAT",
"file_path": "/samples/file_path-72.txt"
},
"ai_decision": {
"id": "id sample 73",
"reason": "reason sample 74",
"confidence": 2.25
},
"selected": true
}
],
"data_security": "ALLOW_ANY",
"required_provider_confidence": "UNTRUSTED",
"required_outbound_data_restriction": {
"restriction": "ONLY_LINKS_FROM_CHAT",
"origin": "MAILBOX",
"reference": "Reference sample 75"
},
"web_addresses_written_by_models": [
"web_addresses_written_by_models sample 77",
"web_addresses_written_by_models sample 76"
],
"name": "name sample 78",
"system_prompt": "system_prompt sample 79",
"blocks": [
{
"time": "2026-10-29T13:50:15+01:00",
"content_type": "UNKNOWN",
"content": {
"$type": "text",
"sources": [
{
"title": "Title sample 81",
"url": "URL sample 82",
"origin": "RAG"
},
{
"title": "Title sample 83",
"url": "URL sample 84",
"origin": "RAG"
}
],
"file_attachments": [
{
"$type": "file",
"type": "IMAGE",
"file_name": "FileName sample 85",
"file_path": "/samples/FilePath-86.txt",
"file_size_bytes": 4000000087,
"is_forbidden": false,
"is_valid": true,
"is_image": true,
"file_path_as_url": "/local/file?path=%2Fsamples%2FFilePath-86.txt",
"provenance_id": "00000000-0000-4000-8000-000000000088",
"version": {
"last_write_utc": "2026-10-29T12:29:00Z",
"length": 4000000090
}
},
{
"$type": "image",
"source_type": "LOCAL_PATH",
"type": "IMAGE",
"file_name": "FileName sample 91",
"file_path": "/samples/FilePath-92.txt",
"file_size_bytes": 4000000093,
"is_forbidden": false,
"is_valid": true,
"is_image": true,
"file_path_as_url": "/local/file?path=%2Fsamples%2FFilePath-92.txt",
"provenance_id": "00000000-0000-4000-8000-000000000094",
"version": {
"last_write_utc": "2026-10-29T12:35:00Z",
"length": 4000000096
}
},
{
"$type": "managed_transcript",
"original_file_name": "OriginalFileName sample 100",
"is_staged": true,
"type": "IMAGE",
"file_name": "FileName sample 97",
"file_path": "/samples/FilePath-98.txt",
"file_size_bytes": 4000000099,
"is_forbidden": false,
"is_valid": true,
"is_image": false,
"file_path_as_url": "/local/file?path=%2Fsamples%2FFilePath-98.txt",
"provenance_id": "00000000-0000-4000-8000-000000000101",
"version": {
"last_write_utc": "2026-10-29T12:42:00Z",
"length": 4000000103
}
}
],
"tool_invocations": [
{
"order": 44,
"tool_id": "tool_id sample 105",
"tool_name": "tool_name sample 106",
"tool_icon": "tool_icon sample 107",
"tool_call_id": "tool_call_id sample 108",
"status": "SUCCESS",
"was_executed": true,
"status_message": "status_message sample 109",
"arguments": {
"argumentsKey sample 110": "arguments sample 111",
"argumentsKey sample 112": "arguments sample 113"
}
},
{
"order": 54,
"tool_id": "tool_id sample 115",
"tool_name": "tool_name sample 116",
"tool_icon": "tool_icon sample 117",
"tool_call_id": "tool_call_id sample 118",
"status": "SUCCESS",
"was_executed": true,
"status_message": "status_message sample 119",
"arguments": {
"argumentsKey sample 120": "arguments sample 121",
"argumentsKey sample 122": "arguments sample 123"
}
}
],
"presentation": {
"kind": "SWOT_ANALYSIS",
"data": {
"sample": 124,
"text": "Gr\u00F6\u00DFe"
}
},
"source_passages": [
{
"source_url": "source_url sample 125",
"data_source_name": "data_source_name sample 126",
"path": "/samples/path-127.txt",
"category": "UNKNOWN",
"type": "UNKNOWN",
"provenance_id": "00000000-0000-4000-8000-000000000128",
"text": "text sample 129",
"surrounding_content": [
"surrounding_content sample 130",
"surrounding_content sample 131"
],
"page_number": 72,
"end_page_number": 73,
"sheet_name": "sheet_name sample 134",
"start_row": 75,
"end_row": 76,
"start_line": 77,
"end_line": 78,
"chunk_number": 79
},
{
"source_url": "source_url sample 140",
"data_source_name": "data_source_name sample 141",
"path": "/samples/path-142.txt",
"category": "UNKNOWN",
"type": "UNKNOWN",
"provenance_id": "00000000-0000-4000-8000-000000000143",
"text": "text sample 144",
"surrounding_content": [
"surrounding_content sample 145",
"surrounding_content sample 146"
],
"page_number": 87,
"end_page_number": 88,
"sheet_name": "sheet_name sample 149",
"start_row": 40,
"end_row": 41,
"start_line": 42,
"end_line": 43,
"chunk_number": 44
}
],
"reported_usage": {
"prompt_tokens": 45,
"model_id": "model_id sample 156",
"block_count": 47
},
"text": "text sample 158"
},
"role": "UNKNOWN",
"hide_from_user": true
},
{
"time": "2026-10-29T15:09:15+01:00",
"content_type": "UNKNOWN",
"content": {
"$type": "image",
"sources": [
{
"title": "Title sample 160",
"url": "URL sample 161",
"origin": "RAG"
},
{
"title": "Title sample 162",
"url": "URL sample 163",
"origin": "RAG"
}
],
"file_attachments": [
{
"$type": "file",
"type": "IMAGE",
"file_name": "FileName sample 164",
"file_path": "/samples/FilePath-165.txt",
"file_size_bytes": 4000000166,
"is_forbidden": false,
"is_valid": true,
"is_image": true,
"file_path_as_url": "/local/file?path=%2Fsamples%2FFilePath-165.txt",
"provenance_id": "00000000-0000-4000-8000-000000000167",
"version": {
"last_write_utc": "2026-10-29T13:48:00Z",
"length": 4000000169
}
},
{
"$type": "image",
"source_type": "LOCAL_PATH",
"type": "IMAGE",
"file_name": "FileName sample 170",
"file_path": "/samples/FilePath-171.txt",
"file_size_bytes": 4000000172,
"is_forbidden": false,
"is_valid": true,
"is_image": true,
"file_path_as_url": "/local/file?path=%2Fsamples%2FFilePath-171.txt",
"provenance_id": "00000000-0000-4000-8000-000000000173",
"version": {
"last_write_utc": "2026-10-29T13:54:00Z",
"length": 4000000175
}
},
{
"$type": "managed_transcript",
"original_file_name": "OriginalFileName sample 179",
"is_staged": true,
"type": "IMAGE",
"file_name": "FileName sample 176",
"file_path": "/samples/FilePath-177.txt",
"file_size_bytes": 4000000178,
"is_forbidden": false,
"is_valid": true,
"is_image": false,
"file_path_as_url": "/local/file?path=%2Fsamples%2FFilePath-177.txt",
"provenance_id": "00000000-0000-4000-8000-000000000180",
"version": {
"last_write_utc": "2026-10-29T14:01:00Z",
"length": 4000000182
}
}
],
"source_type": "LOCAL_PATH",
"source": "source sample 183"
},
"role": "UNKNOWN",
"hide_from_user": true
}
]
}
@@ -0,0 +1,21 @@
{
"format_version": 41,
"app_version": "AppVersion sample 2",
"settings_version": "SettingsVersion sample 3",
"created_at_utc": "2026-10-29T12:34:15+01:00",
"source_data_directory": "SourceDataDirectory sample 5",
"source_config_directory": "SourceConfigDirectory sample 6",
"includes_data_source_indexes": true,
"entries": [
{
"name": "Name sample 7",
"size": 4000000008,
"sha256": "Sha256 sample 9"
},
{
"name": "Name sample 10",
"size": 4000000011,
"sha256": "Sha256 sample 12"
}
]
}
@@ -0,0 +1,24 @@
{
"format_version": 41,
"prepared_at_utc": "2026-10-29T12:32:15+01:00",
"backup_created_at_utc": "2026-10-29T12:33:15+01:00",
"backup_app_version": "BackupAppVersion sample 4",
"elements": [
{
"location": "CONFIG_DIRECTORY",
"relative_path": "/samples/RelativePath-5.txt",
"keep": [
"Keep sample 6",
"Keep sample 7"
]
},
{
"location": "CONFIG_DIRECTORY",
"relative_path": "/samples/RelativePath-8.txt",
"keep": [
"Keep sample 9",
"Keep sample 10"
]
}
]
}
@@ -0,0 +1,8 @@
{
"outcome": "REFUSED",
"detail": "Detail sample 1",
"prepared_at_utc": "PreparedAtUtc sample 2",
"backup_created_at_utc": "BackupCreatedAtUtc sample 3",
"backup_app_version": "BackupAppVersion sample 4",
"finished_at_utc": "FinishedAtUtc sample 5"
}
@@ -0,0 +1 @@
["Vor dem Treffer: <b>Größe</b> & \"Zitat\" für C# 14","Nach dem Treffer: 50 % – fertig, 'oder?'"]
@@ -0,0 +1,71 @@
{
"Version": "V1",
"Providers": [
{
"Num": 7,
"Id": "Id sample 1",
"InstanceName": "InstanceName sample 2",
"UsedProvider": "OPEN_AI",
"Model": {
"Id": "Id sample 3",
"DisplayName": "DisplayName sample 4",
"IsSystemModel": false
},
"IsSelfHosted": true,
"Hostname": "Hostname sample 5",
"Host": "LM_STUDIO"
},
{
"Num": 7,
"Id": "Id sample 6",
"InstanceName": "InstanceName sample 7",
"UsedProvider": "OPEN_AI",
"Model": {
"Id": "Id sample 8",
"DisplayName": "DisplayName sample 9",
"IsSystemModel": false
},
"IsSelfHosted": true,
"Hostname": "Hostname sample 10",
"Host": "LM_STUDIO"
}
],
"NextProviderNum": 7,
"IsSavingEnergy": true,
"EnableSpellchecking": true,
"UpdateInterval": "DISABLE_UPDATES",
"NavigationBehavior": "NEVER_EXPAND_USE_TOOLTIPS",
"ShortcutSendBehavior": "MODIFER_ENTER_IS_SENDING",
"PreselectChatOptions": true,
"PreselectedChatProvider": "PreselectedChatProvider sample 11",
"WorkspaceStorageBehavior": "STORE_CHATS_AUTOMATICALLY",
"WorkspaceStorageTemporaryMaintenancePolicy": "DELETE_OLDER_THAN_7_DAYS",
"PreselectIconOptions": true,
"PreselectedIconSource": "ICONS8",
"PreselectedIconProvider": "PreselectedIconProvider sample 12",
"LiveTranslationDebounceIntervalMilliseconds": 53,
"PreselectTranslationOptions": true,
"PreselectLiveTranslation": true,
"HideWebContentReaderForTranslation": true,
"PreselectWebContentReaderForTranslation": true,
"PreselectContentCleanerAgentForTranslation": true,
"PreselectedTranslationTargetLanguage": "EN_US",
"PreselectTranslationOtherLanguage": "PreselectTranslationOtherLanguage sample 14",
"PreselectedTranslationProvider": "PreselectedTranslationProvider sample 15",
"PreselectCodingOptions": true,
"PreselectCodingCompilerMessages": true,
"PreselectedCodingLanguage": "BASH",
"PreselectedCodingOtherLanguage": "PreselectedCodingOtherLanguage sample 16",
"PreselectedCodingProvider": "PreselectedCodingProvider sample 17",
"PreselectTextSummarizerOptions": true,
"HideWebContentReaderForTextSummarizer": true,
"PreselectWebContentReaderForTextSummarizer": true,
"PreselectContentCleanerAgentForTextSummarizer": true,
"PreselectedTextSummarizerTargetLanguage": "EN_US",
"PreselectedTextSummarizerOtherLanguage": "PreselectedTextSummarizerOtherLanguage sample 18",
"PreselectedTextSummarizerComplexity": "SIMPLE_LANGUAGE",
"PreselectedTextSummarizerExpertInField": "PreselectedTextSummarizerExpertInField sample 19",
"PreselectedTextSummarizerProvider": "PreselectedTextSummarizerProvider sample 20",
"PreselectAgentTextContentCleanerOptions": true,
"PreselectedAgentTextContentCleanerProvider": "PreselectedAgentTextContentCleanerProvider sample 21"
}
@@ -0,0 +1,305 @@
{
"Version": "V4",
"Providers": [
{
"Num": 7,
"Id": "Id sample 1",
"InstanceName": "InstanceName sample 2",
"UsedProvider": "OPEN_AI",
"Model": {
"Id": "Id sample 3",
"DisplayName": "DisplayName sample 4",
"IsSystemModel": false
},
"IsSelfHosted": true,
"Hostname": "Hostname sample 5",
"Host": "LM_STUDIO"
},
{
"Num": 7,
"Id": "Id sample 6",
"InstanceName": "InstanceName sample 7",
"UsedProvider": "OPEN_AI",
"Model": {
"Id": "Id sample 8",
"DisplayName": "DisplayName sample 9",
"IsSystemModel": false
},
"IsSelfHosted": true,
"Hostname": "Hostname sample 10",
"Host": "LM_STUDIO"
}
],
"LLMProviders": {
"EnforceGlobalMinimumConfidence": true,
"GlobalMinimumConfidence": "UNTRUSTED",
"ShowProviderConfidence": true,
"ConfidenceScheme": "TRUST_USA_EUROPE",
"CustomConfidenceScheme": {
"OPEN_AI": "UNTRUSTED",
"ANTHROPIC": "UNKNOWN"
}
},
"Profiles": [
{
"Num": 7,
"Id": "Id sample 11",
"Name": "Name sample 12",
"NeedToKnow": "NeedToKnow sample 13",
"Actions": "Actions sample 14",
"IsEnterpriseConfiguration": true,
"EnterpriseConfigurationPluginId": "00000000-0000-4000-8000-000000000015"
},
{
"Num": 7,
"Id": "Id sample 16",
"Name": "Name sample 17",
"NeedToKnow": "NeedToKnow sample 18",
"Actions": "Actions sample 19",
"IsEnterpriseConfiguration": true,
"EnterpriseConfigurationPluginId": "00000000-0000-4000-8000-000000000020"
}
],
"NextProviderNum": 7,
"NextProfileNum": 7,
"App": {
"LanguageBehavior": "MANUAL",
"LanguagePluginId": "00000000-0000-4000-8000-000000000021",
"PreferredTheme": "LIGHT",
"IsSavingEnergy": true,
"EnableSpellchecking": true,
"CloseExportMessagesAutomatically": true,
"UpdateInterval": "DISABLE_UPDATES",
"UpdateInstallation": "AUTOMATIC",
"NavigationBehavior": "NEVER_EXPAND_USE_TOOLTIPS",
"StartPage": "CHAT",
"InterfaceScreenSizes": [
{
"ScreenKey": "ScreenKey sample 22",
"Size": "SYSTEM_SCALING",
"CustomPercent": 63
},
{
"ScreenKey": "ScreenKey sample 24",
"Size": "SYSTEM_SCALING",
"CustomPercent": 65
}
],
"ShowPromptInjectionAlert": true,
"ShowIntroduction": true,
"ShowQuickStartGuide": true,
"ShowLastChangelog": true,
"ShowVision": true,
"PreviewVisibility": "RELEASE_CANDIDATE",
"EnabledPreviewFeatures": [
"PRE_WRITER_MODE_2024",
"PRE_RAG_2024"
],
"PreselectedProvider": "PreselectedProvider sample 26",
"PreselectedProfile": "PreselectedProfile sample 27",
"PreselectedChatTemplate": "PreselectedChatTemplate sample 28",
"UseTranscriptionProvider": "UseTranscriptionProvider sample 29",
"OpusBitrate": "KBPS_32",
"ShortcutVoiceRecording": "ShortcutVoiceRecording sample 30",
"ShortcutVoiceRecordingDisplayName": "ShortcutVoiceRecordingDisplayName sample 31",
"ShortcutVoiceRecordingDisplaySource": "ShortcutVoiceRecordingDisplaySource sample 32",
"HttpClientTimeoutSeconds": 73,
"ShareFeatureUsageWithSelfHostedServerOperators": true,
"ExternalHttpCustomRootCertificatesEnabled": true,
"ExternalHttpCustomRootCertificateBundlePath": "/samples/ExternalHttpCustomRootCertificateBundlePath-34.txt",
"ExternalHttpCustomRootCertificateAllowedHosts": [
"ExternalHttpCustomRootCertificateAllowedHosts sample 35",
"ExternalHttpCustomRootCertificateAllowedHosts sample 36"
],
"AllowUserToAddProvider": true,
"AllowUserToAddLLMProvider": true,
"AllowUserToAddEmbeddingProvider": true,
"AllowUserToAddTranscriptionProvider": true,
"ProhibitedProviders": [
"OPEN_AI",
"ANTHROPIC"
],
"AllowUserToAddMailbox": true,
"AllowUserToImportConfigurationSnippets": true,
"AllowUserToImportProfile": true,
"AllowUserToImportLLMProvider": true,
"AllowUserToImportEmbeddingProvider": true,
"AllowUserToImportTranscriptionProvider": true,
"AllowUserToImportChatTemplate": true,
"AllowUserToImportERIDataSource": true,
"AllowUserToImportDocumentAnalysisPolicy": true,
"AllowUserToImportPlugins": true,
"AllowUserToImportConfigurationPlugins": true,
"AllowUserToSharePlugins": true,
"AllowUserToBackUpData": true,
"AllowUserToRestoreData": true,
"ShowAdminSettings": true,
"EnterpriseEncryptionExportFormat": "V2",
"AddAIGeneratedContentDisclosure": true,
"DataSourceIndexing": {
"AutomaticRefresh": true,
"MinimumPauseBetweenStepsSeconds": 77,
"MaximumPauseBetweenStepsSeconds": 78
},
"HiddenAssistants": [
"UNKNOWN",
"GRAMMAR_SPELLING_ASSISTANT"
]
},
"Chat": {
"ShortcutSendBehavior": "MODIFER_ENTER_IS_SENDING",
"LoadingProviderBehavior": "ALWAYS_USE_DEFAULT_CHAT_PROVIDER",
"AddChatProviderBehavior": "ADDED_CHATS_USE_LATEST_PROVIDER",
"SendToChatDataSourceBehavior": "APPLY_STANDARD_CHAT_DATA_SOURCE_OPTIONS",
"PreselectOptions": true,
"PreselectedProvider": "PreselectedProvider sample 39",
"PreselectedProfile": "PreselectedProfile sample 40",
"PreselectedChatTemplate": "PreselectedChatTemplate sample 41",
"PreselectedDataSourcesDisabled": true,
"PreselectedDataSourcesAutomaticSelection": true,
"PreselectedDataSourcesAutomaticValidation": true,
"PreselectedDataSourceIds": [
"PreselectedDataSourceIds sample 44",
"PreselectedDataSourceIds sample 45"
],
"PreselectedDataSourcesRetrievalMode": "EVERY_MESSAGE",
"PreselectedDataSourceOptions": {
"DisableDataSources": true,
"AutomaticDataSourceSelection": true,
"AutomaticValidation": true,
"RetrievalMode": "EVERY_MESSAGE",
"PreselectedDataSourceIds": [
"PreselectedDataSourceIds sample 44",
"PreselectedDataSourceIds sample 45"
]
},
"ShowLatestMessageAfterLoading": true,
"ReadChangedAttachmentsAgain": true
},
"Workspace": {
"StorageBehavior": "STORE_CHATS_AUTOMATICALLY",
"StorageTemporaryMaintenancePolicy": "DELETE_OLDER_THAN_7_DAYS",
"DisplayBehavior": "TOGGLE_SIDEBAR",
"IsSidebarVisible": true,
"SplitterPosition": 1.5
},
"IconFinder": {
"PreselectOptions": true,
"PreselectedSource": "ICONS8",
"MinimumProviderConfidence": "UNTRUSTED",
"PreselectedProvider": "PreselectedProvider sample 46"
},
"Translation": {
"DebounceIntervalMilliseconds": 87,
"PreselectOptions": true,
"PreselectLiveTranslation": true,
"HideWebContentReader": true,
"PreselectWebContentReader": true,
"PreselectContentCleanerAgent": true,
"PreselectedTargetLanguage": "EN_US",
"PreselectOtherLanguage": "PreselectOtherLanguage sample 48",
"MinimumProviderConfidence": "UNTRUSTED",
"PreselectedProvider": "PreselectedProvider sample 49"
},
"Coding": {
"PreselectOptions": true,
"PreselectCompilerMessages": true,
"PreselectedProgrammingLanguage": "BASH",
"PreselectedOtherProgrammingLanguage": "PreselectedOtherProgrammingLanguage sample 50",
"MinimumProviderConfidence": "UNTRUSTED",
"PreselectedProvider": "PreselectedProvider sample 51",
"PreselectedProfile": "PreselectedProfile sample 52"
},
"TextSummarizer": {
"PreselectOptions": true,
"HideWebContentReader": true,
"PreselectWebContentReader": true,
"PreselectContentCleanerAgent": true,
"PreselectedTargetLanguage": "EN_US",
"PreselectedOtherLanguage": "PreselectedOtherLanguage sample 53",
"PreselectedComplexity": "SIMPLE_LANGUAGE",
"PreselectedExpertInField": "PreselectedExpertInField sample 54",
"PreselectedImportantAspects": "PreselectedImportantAspects sample 55",
"MinimumProviderConfidence": "UNTRUSTED",
"PreselectedProvider": "PreselectedProvider sample 56"
},
"TextContentCleaner": {
"PreselectAgentOptions": true,
"PreselectedAgentProvider": "PreselectedAgentProvider sample 57"
},
"Agenda": {
"PreselectOptions": true,
"PreselectName": "PreselectName sample 58",
"PreselectTopic": "PreselectTopic sample 59",
"PreselectObjective": "PreselectObjective sample 60",
"PreselectModerator": "PreselectModerator sample 61",
"PreselectDuration": "PreselectDuration sample 62",
"PreselectStartTime": "PreselectStartTime sample 63",
"PreselectIntroduceParticipants": true,
"PreselectNumberParticipants": "PEER_TO_PEER",
"PreselectActiveParticipation": true,
"PreselectIsMeetingVirtual": true,
"PreselectLocation": "PreselectLocation sample 64",
"PreselectJointDinner": true,
"PreselectSocialActivity": true,
"PreselectArriveAndDepart": true,
"PreselectLunchTime": 55,
"PreselectBreakTime": 56,
"PreselectedTargetLanguage": "EN_US",
"PreselectedOtherLanguage": "PreselectedOtherLanguage sample 67",
"MinimumProviderConfidence": "UNTRUSTED",
"PreselectedProvider": "PreselectedProvider sample 68",
"PreselectedProfile": "PreselectedProfile sample 69"
},
"GrammarSpelling": {
"PreselectOptions": true,
"PreselectedTargetLanguage": "EN_US",
"PreselectedOtherLanguage": "PreselectedOtherLanguage sample 70",
"MinimumProviderConfidence": "UNTRUSTED",
"PreselectedProvider": "PreselectedProvider sample 71"
},
"RewriteImprove": {
"PreselectOptions": true,
"PreselectedTargetLanguage": "EN_US",
"PreselectedOtherLanguage": "PreselectedOtherLanguage sample 72",
"PreselectedWritingStyle": "EVERYDAY",
"PreselectedSentenceStructure": "ACTIVE",
"MinimumProviderConfidence": "UNTRUSTED",
"PreselectedProvider": "PreselectedProvider sample 73"
},
"EMail": {
"PreselectOptions": true,
"PreselectedTargetLanguage": "EN_US",
"PreselectOtherLanguage": "PreselectOtherLanguage sample 74",
"PreselectedWritingStyle": "BUSINESS_FORMAL",
"Greeting": "Greeting sample 75",
"SenderName": "SenderName sample 76",
"MinimumProviderConfidence": "UNTRUSTED",
"PreselectedProvider": "PreselectedProvider sample 77",
"PreselectedProfile": "PreselectedProfile sample 78"
},
"LegalCheck": {
"PreselectOptions": true,
"HideWebContentReader": true,
"PreselectWebContentReader": true,
"PreselectContentCleanerAgent": true,
"MinimumProviderConfidence": "UNTRUSTED",
"PreselectedProvider": "PreselectedProvider sample 79",
"PreselectedProfile": "PreselectedProfile sample 80"
},
"Synonyms": {
"PreselectOptions": true,
"PreselectedLanguage": "EN_US",
"PreselectedOtherLanguage": "PreselectedOtherLanguage sample 81",
"MinimumProviderConfidence": "UNTRUSTED",
"PreselectedProvider": "PreselectedProvider sample 82"
},
"MyTasks": {
"PreselectOptions": true,
"PreselectedTargetLanguage": "EN_US",
"PreselectOtherLanguage": "PreselectOtherLanguage sample 83",
"MinimumProviderConfidence": "UNTRUSTED",
"PreselectedProvider": "PreselectedProvider sample 84",
"PreselectedProfile": "PreselectedProfile sample 85"
}
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,103 @@
{
"matrix_heading": "matrix_heading sample 1",
"internal_label": "internal_label sample 2",
"external_label": "external_label sample 3",
"positive_label": "positive_label sample 4",
"negative_label": "negative_label sample 5",
"strengths": {
"label": "label sample 6",
"empty_message": "empty_message sample 7",
"findings": [
{
"summary": "summary sample 8",
"explanation": "explanation sample 9"
},
{
"summary": "summary sample 10",
"explanation": "explanation sample 11"
}
]
},
"weaknesses": {
"label": "label sample 12",
"empty_message": "empty_message sample 13",
"findings": [
{
"summary": "summary sample 14",
"explanation": "explanation sample 15"
},
{
"summary": "summary sample 16",
"explanation": "explanation sample 17"
}
]
},
"opportunities": {
"label": "label sample 18",
"empty_message": "empty_message sample 19",
"findings": [
{
"summary": "summary sample 20",
"explanation": "explanation sample 21"
},
{
"summary": "summary sample 22",
"explanation": "explanation sample 23"
}
]
},
"threats": {
"label": "label sample 24",
"empty_message": "empty_message sample 25",
"findings": [
{
"summary": "summary sample 26",
"explanation": "explanation sample 27"
},
{
"summary": "summary sample 28",
"explanation": "explanation sample 29"
}
]
},
"overall_assessment": {
"label": "label sample 30",
"text": "text sample 31"
},
"prioritized_actions": {
"label": "label sample 32",
"empty_message": "empty_message sample 33",
"items": [
{
"action": "action sample 34",
"rationale": "rationale sample 35",
"addressed_factors": [
"addressed_factors sample 36",
"addressed_factors sample 37"
]
},
{
"action": "action sample 38",
"rationale": "rationale sample 39",
"addressed_factors": [
"addressed_factors sample 40",
"addressed_factors sample 41"
]
}
]
},
"moderation_notes": {
"label": "label sample 42",
"empty_message": "empty_message sample 43",
"items": [
{
"change": "change sample 44",
"reason": "reason sample 45"
},
{
"change": "change sample 46",
"reason": "reason sample 47"
}
]
}
}
@@ -0,0 +1,67 @@
using System.Text.Json.Nodes;
namespace AIStudio.Tests.Serialization;
/// <summary>
/// Finds the values of one JSON document which another one lacks.
/// </summary>
public static class JsonContainment
{
/// <summary>
/// Returns the path of every value in the expected document which the actual one lacks or holds otherwise.
/// </summary>
/// <remarks>
/// Properties only the actual document has don't count, e.g., one added to a type after the expected document
/// was written. A property which is null in the expected document may be missing in the actual one, since
/// some properties are only written with a value. Arrays have to hold the same elements, in any order: an
/// ImmutableHashSet of strings orders them by their hash codes, which differ from process to process.
/// </remarks>
/// <param name="expected">The document whose values have to be there.</param>
/// <param name="actual">The document which has to hold them.</param>
/// <returns>One line per value, with its path and both values.</returns>
public static IReadOnlyList<string> FindMissing(JsonNode? expected, JsonNode? actual)
{
var missing = new List<string>();
Compare(expected, actual, "$", missing);
return missing;
}
private static void Compare(JsonNode? expected, JsonNode? actual, string path, List<string> missing)
{
switch (expected)
{
case JsonObject expectedObject when actual is JsonObject actualObject:
foreach (var (name, expectedValue) in expectedObject)
{
if (actualObject.TryGetPropertyValue(name, out var actualValue))
Compare(expectedValue, actualValue, $"{path}.{name}", missing);
else if (expectedValue is not null)
missing.Add($"{path}.{name}: missing, expected {expectedValue.ToJsonString()}");
}
break;
case JsonArray expectedArray when actual is JsonArray actualArray && expectedArray.Count == actualArray.Count:
var unmatched = actualArray.ToList();
for (var index = 0; index < expectedArray.Count; index++)
{
var match = unmatched.FindIndex(candidate => FindMissing(expectedArray[index], candidate).Count is 0);
if (match < 0)
{
missing.Add($"{path}[{index}]: no element holds {expectedArray[index]?.ToJsonString() ?? "null"}");
continue;
}
unmatched.RemoveAt(match);
}
break;
default:
if (!JsonNode.DeepEquals(expected, actual))
missing.Add($"{path}: {actual?.ToJsonString() ?? "null"}, expected {expected?.ToJsonString() ?? "null"}");
break;
}
}
}
@@ -0,0 +1,59 @@
using System.Text;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
namespace AIStudio.Tests.Serialization;
/// <summary>
/// Describes the contract System.Text.Json uses for a type as text, so that two contracts can be compared and
/// a difference shows up readable in the message of a test.
/// </summary>
public static class JsonContractDescription
{
/// <summary>
/// Describes everything in a contract which decides what gets written and what a read accepts.
/// </summary>
/// <param name="typeInfo">The contract.</param>
/// <returns>One line for the type, and one line per property, polymorphism, and callback.</returns>
public static string Describe(JsonTypeInfo typeInfo)
{
var text = new StringBuilder();
text.AppendLine($"{TypeName(typeInfo.Type)}: {typeInfo.Kind}, converter {ConverterName(typeInfo.Converter)}, created {(typeInfo.CreateObject is null ? "otherwise" : "without arguments")}");
text.AppendLine($" handling: numbers {typeInfo.NumberHandling}, unmapped members {typeInfo.UnmappedMemberHandling}, object creation {typeInfo.PreferredPropertyObjectCreationHandling}");
text.AppendLine($" callbacks: serializing {typeInfo.OnSerializing is not null}, serialized {typeInfo.OnSerialized is not null}, deserializing {typeInfo.OnDeserializing is not null}, deserialized {typeInfo.OnDeserialized is not null}");
if (typeInfo.KeyType is { } keyType)
text.AppendLine($" key: {TypeName(keyType)}");
if (typeInfo.ElementType is { } elementType)
text.AppendLine($" element: {TypeName(elementType)}");
if (typeInfo.PolymorphismOptions is { } polymorphism)
{
text.AppendLine($" polymorphism: discriminator {polymorphism.TypeDiscriminatorPropertyName}, unrecognized ignored {polymorphism.IgnoreUnrecognizedTypeDiscriminators}, unknown types {polymorphism.UnknownDerivedTypeHandling}");
foreach (var derivedType in polymorphism.DerivedTypes)
text.AppendLine($" derived: {TypeName(derivedType.DerivedType)} as {derivedType.TypeDiscriminator}");
}
foreach (var property in typeInfo.Properties)
text.AppendLine($" property {property.Name}: {TypeName(property.PropertyType)}, order {property.Order}, get {property.Get is not null}, set {property.Set is not null}, required {property.IsRequired}, extension data {property.IsExtensionData}, conditional {property.ShouldSerialize is not null}, converter {ConverterName(property.CustomConverter)}, constructor parameter {property.AssociatedParameter?.Name ?? "none"}, nullable get {property.IsGetNullable}, nullable set {property.IsSetNullable}, numbers {property.NumberHandling}, object creation {property.ObjectCreationHandling}");
return text.ToString();
}
/// <summary>
/// Returns the name of a type the way C# writes it, e.g., Dictionary&lt;String, Int32&gt;.
/// </summary>
/// <param name="type">The type.</param>
public static string TypeName(Type type)
{
var arity = type.Name.IndexOf('`');
var name = arity < 0 ? type.Name : $"{type.Name[..arity]}<{string.Join(", ", type.GetGenericArguments().Select(TypeName))}>";
return type is { IsNested: true, IsGenericParameter: false } ?$"{TypeName(type.DeclaringType!)}.{name}" : name;
}
//
// Reflection reads the constructor parameters of a record through converters of its own, whose names end
// with "WithReflection". Source generation does the same with the plain ones, which read the same JSON:
//
private static string ConverterName(JsonConverter? converter) => converter is null ? "none" : TypeName(converter.GetType()).Replace("WithReflection", string.Empty, StringComparison.Ordinal);
}
@@ -0,0 +1,23 @@
namespace AIStudio.Tests.Serialization;
/// <summary>
/// Checks that the contracts AI Studio uses for its JSON match those reflection builds.
/// </summary>
/// <remarks>
/// AI Studio wrote all its files with contracts which reflection built. Any other contract, e.g., one which
/// source generation builds, has to match them in every detail, or AI Studio no longer reads what it wrote
/// before: a property with another name, without a setter, or without its converter silently loses its value.
/// </remarks>
[TestFixture]
public sealed class JsonContractTests
{
[TestCaseSource(typeof(JsonTestRoots), nameof(JsonTestRoots.Stored))]
public void TheContractsMatchThoseOfReflection(JsonTestRoot root)
{
var baseline = JsonReflectionBaseline.Of(root.Options);
var expected = JsonTypeGraph.Collect(root.Type, baseline).Select(type => JsonContractDescription.Describe(baseline.GetTypeInfo(type)));
var actual = JsonTypeGraph.Collect(root.Type, root.Options).Select(type => JsonContractDescription.Describe(root.Options.GetTypeInfo(type)));
Assert.That(actual, Is.EqualTo(expected));
}
}
@@ -0,0 +1,61 @@
using System.Text.Json;
using System.Text.Json.Serialization.Metadata;
namespace AIStudio.Tests.Serialization;
/// <summary>
/// Checks that a property missing in the JSON keeps the value a new object starts with.
/// </summary>
/// <remarks>
/// Files written by an older version of AI Studio lack every property added since. 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 (dotnet/runtime#84484).
/// Hence, the types AI Studio stores use set instead of init.
/// </remarks>
[TestFixture]
public sealed class JsonDefaultsTests
{
[TestCaseSource(typeof(JsonTestRoots), nameof(JsonTestRoots.Stored))]
public void AnEmptyObjectGetsTheDefaultsOfANewOne(JsonTestRoot root)
{
var failures = new List<string>();
foreach (var type in JsonTypeGraph.Collect(root.Type, root.Options))
{
//
// A type which gets created through its constructor receives every value as an argument, so nothing
// gets lost there. A required property refuses an empty object anyway:
//
var typeInfo = root.Options.GetTypeInfo(type);
if (typeInfo is not { Kind: JsonTypeInfoKind.Object, CreateObject: { } createObject } || typeInfo.Properties.Any(property => property.IsRequired))
continue;
var newObject = createObject();
var otherNewObject = createObject();
var readObject = JsonSerializer.Deserialize("{}", typeInfo)!;
foreach (var property in typeInfo.Properties.Where(property => property.Get is not null))
{
var expected = ValueAsJson(property, newObject, root.Options);
var actual = ValueAsJson(property, readObject, root.Options);
//
// Some properties start with a value of their own each time, e.g., a new ID. There, the read object
// just needs any value but the default of the type:
//
if (expected == ValueAsJson(property, otherNewObject, root.Options))
{
if (actual != expected)
failures.Add($"{JsonContractDescription.TypeName(type)}.{property.Name}: read as {actual}, while a new object has {expected}.");
}
else if (actual == DefaultAsJson(property.PropertyType, root.Options))
failures.Add($"{JsonContractDescription.TypeName(type)}.{property.Name}: read as {actual}, while a new object gets a value of its own.");
}
}
Assert.That(failures, Is.Empty);
}
private static string ValueAsJson(JsonPropertyInfo property, object instance, JsonSerializerOptions options) => JsonSerializer.Serialize(property.Get!(instance), property.PropertyType, options);
private static string DefaultAsJson(Type type, JsonSerializerOptions options) => type.IsValueType ? JsonSerializer.Serialize(Activator.CreateInstance(type), type, options) : "null";
}
+15
View File
@@ -0,0 +1,15 @@
using System.Text.Json;
namespace AIStudio.Tests.Serialization;
/// <summary>
/// A stored file in the fixtures, together with the type and the options AI Studio reads it with.
/// </summary>
/// <param name="FileName">The name of the file in the folder Fixtures next to the tests.</param>
/// <param name="Type">The type AI Studio reads the file as.</param>
/// <param name="Options">The options AI Studio reads and writes the file with.</param>
public sealed record JsonGoldenFile(string FileName, Type Type, JsonSerializerOptions Options)
{
/// <inheritdoc />
public override string ToString() => this.FileName;
}
@@ -0,0 +1,89 @@
using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Text.Json.Nodes;
using AIStudio.Assistants.SWOTAnalysis;
using AIStudio.Chat;
using AIStudio.Settings;
using AIStudio.Settings.DataModel;
using AIStudio.Settings.DataModel.PreviousModels;
using AIStudio.Tools;
using AIStudio.Tools.Databases.ProvenanceStore;
using AIStudio.Tools.DataBackup;
using Microsoft.Extensions.Logging.Abstractions;
using Version = AIStudio.Settings.Version;
namespace AIStudio.Tests.Serialization;
/// <summary>
/// Reads and writes stored files of AI Studio, and checks that nothing gets lost and nothing gets written
/// otherwise than with the contracts of reflection.
/// </summary>
/// <remarks>
/// The files in the folder Fixtures were written with the contracts of reflection, before AI Studio got
/// contracts of its own, and every property holds a value. They are made up: no real chat or setting of
/// anybody is among them. A property added later is missing in them, which the tests allow.
/// </remarks>
[TestFixture]
public sealed class JsonGoldenFileTests
{
private static IEnumerable<JsonGoldenFile> GoldenFiles()
{
TestHost.PrepareStaticState();
yield return new("settings.json", typeof(Data), SettingsManager.JSON_OPTIONS);
yield return new("settings-v1.json", typeof(DataV1V3), SettingsManager.JSON_OPTIONS);
yield return new("settings-v4.json", typeof(DataV4), SettingsManager.JSON_OPTIONS);
yield return new("settings-v5.json", typeof(DataV5), SettingsManager.JSON_OPTIONS);
yield return new("chat.json", typeof(ChatThread), WorkspaceBehaviour.JSON_OPTIONS);
yield return new("swot-analysis.json", typeof(SwotAnalysisResult), SwotAnalysisContentPresentation.JSON_OPTIONS);
yield return new("data-backup-manifest.json", typeof(DataBackupManifest), DataBackupFormat.JSON_OPTIONS);
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<string>), SqliteProvenanceStoreClientImplementation.JSON_OPTIONS);
}
[TestCaseSource(nameof(GoldenFiles))]
public void AStoredFileIsWrittenAgainAsWithReflection(JsonGoldenFile goldenFile)
{
var json = ReadFixture(goldenFile.FileName);
var baseline = JsonReflectionBaseline.Of(goldenFile.Options);
var writtenAgain = JsonSerializer.Serialize(JsonSerializer.Deserialize(json, goldenFile.Type, goldenFile.Options), goldenFile.Type, goldenFile.Options);
var writtenAgainByReflection = JsonSerializer.Serialize(JsonSerializer.Deserialize(json, goldenFile.Type, baseline), goldenFile.Type, baseline);
Assert.That(writtenAgain, Is.EqualTo(writtenAgainByReflection));
}
[TestCaseSource(nameof(GoldenFiles))]
public void AStoredFileKeepsAllItsValues(JsonGoldenFile goldenFile)
{
var json = ReadFixture(goldenFile.FileName);
var writtenAgain = JsonSerializer.Serialize(JsonSerializer.Deserialize(json, goldenFile.Type, goldenFile.Options), goldenFile.Type, goldenFile.Options);
Assert.That(JsonContainment.FindMissing(JsonNode.Parse(json), JsonNode.Parse(writtenAgain)), Is.Empty);
}
[TestCase("settings-v1.json", Version.V1)]
[TestCase("settings-v4.json", Version.V4)]
[TestCase("settings-v5.json", Version.V5)]
public void OldSettingsMigrateAsWithReflection(string fileName, Version version)
{
var json = ReadFixture(fileName);
var baseline = JsonReflectionBaseline.Of(SettingsManager.JSON_OPTIONS);
var migrated = SettingsMigrations.Migrate(NullLogger<SettingsManager>.Instance, version, json, SettingsManager.JSON_OPTIONS);
var migratedByReflection = SettingsMigrations.Migrate(NullLogger<SettingsManager>.Instance, version, json, baseline);
Assert.That(JsonSerializer.Serialize(migrated, SettingsManager.JSON_OPTIONS), Is.EqualTo(JsonSerializer.Serialize(migratedByReflection, baseline)));
}
/// <summary>
/// Reads a file from the fixtures next to this test.
/// </summary>
/// <remarks>
/// Read from the source tree, the way the other fixtures are, so the file needs no entry in the project file.
/// </remarks>
private static string ReadFixture(string fileName, [CallerFilePath] string sourceFilePath = "") => File.ReadAllText(Path.Combine(Path.GetDirectoryName(sourceFilePath)!, "Fixtures", fileName));
}
@@ -0,0 +1,27 @@
using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Text.Json.Serialization.Metadata;
namespace AIStudio.Tests.Serialization;
/// <summary>
/// The options of AI Studio as System.Text.Json uses them without source generation: the same settings, but
/// with contracts which reflection builds.
/// </summary>
/// <remarks>
/// AI Studio stored all its files with such contracts, so they are the baseline every other contract has to
/// match. Each baseline is built once per options, since reflection builds its contracts once per options, too.
/// </remarks>
public static class JsonReflectionBaseline
{
private static readonly ConditionalWeakTable<JsonSerializerOptions, JsonSerializerOptions> BASELINES = new();
/// <summary>
/// Returns a copy of the given options, whose contracts reflection builds.
/// </summary>
/// <param name="options">The options AI Studio uses.</param>
public static JsonSerializerOptions Of(JsonSerializerOptions options) => BASELINES.GetValue(options, static original => new JsonSerializerOptions(original)
{
TypeInfoResolver = new DefaultJsonTypeInfoResolver(),
});
}
+14
View File
@@ -0,0 +1,14 @@
using System.Text.Json;
namespace AIStudio.Tests.Serialization;
/// <summary>
/// A type which AI Studio reads or writes as JSON on its own, together with the options it uses for that.
/// </summary>
/// <param name="Type">The type which gets serialized. Whatever it holds gets checked along with it.</param>
/// <param name="Options">The options AI Studio passes to System.Text.Json for this type.</param>
public sealed record JsonTestRoot(Type Type, JsonSerializerOptions Options)
{
/// <inheritdoc />
public override string ToString() => JsonContractDescription.TypeName(this.Type);
}
+80
View File
@@ -0,0 +1,80 @@
using AIStudio.Assistants.SWOTAnalysis;
using AIStudio.Chat;
using AIStudio.Settings;
using AIStudio.Settings.DataModel;
using AIStudio.Settings.DataModel.PreviousModels;
using AIStudio.Tools;
using AIStudio.Tools.Databases.ProvenanceStore;
using AIStudio.Tools.DataBackup;
namespace AIStudio.Tests.Serialization;
/// <summary>
/// The types which AI Studio reads or writes as JSON on their own, for the tests which compare the contracts of
/// System.Text.Json.
/// </summary>
/// <remarks>
/// Only the outermost types belong here: whatever they hold gets checked along with them. Add a type whenever
/// AI Studio starts to store a new kind of file, with the options it stores the file with.
/// </remarks>
public static class JsonTestRoots
{
/// <summary>
/// The data AI Studio keeps on the disk of the user.
/// </summary>
public static IEnumerable<TestCaseData> Stored()
{
TestHost.PrepareStaticState();
// Chats, in the workspaces, among the temporary chats, and in chat archives:
yield return Case(new(typeof(ChatThread), WorkspaceBehaviour.JSON_OPTIONS));
// The settings, and their older versions, which the migrations still read:
yield return Case(new(typeof(Data), SettingsManager.JSON_OPTIONS));
yield return Case(new(typeof(DataV1V3), SettingsManager.JSON_OPTIONS));
yield return Case(new(typeof(DataV4), SettingsManager.JSON_OPTIONS));
yield return Case(new(typeof(DataV5), SettingsManager.JSON_OPTIONS));
//
// The values of managed settings, which get written and read on their own, as the type of their
// setting. That type is often an interface, such as ISet<string>, which the settings themselves
// never use:
//
foreach (var valueType in ManagedSettingValueTypes())
yield return Case(new(valueType, SettingsManager.JSON_OPTIONS));
// The SWOT analysis, which a chat stores as the presentation of a message:
yield return Case(new(typeof(SwotAnalysisResult), SwotAnalysisContentPresentation.JSON_OPTIONS));
// Backups and their restore:
yield return Case(new(typeof(DataBackupManifest), DataBackupFormat.JSON_OPTIONS));
yield return Case(new(typeof(DataRestorePlan), DataBackupFormat.JSON_OPTIONS));
yield return Case(new(typeof(DataRestoreReport), DataBackupFormat.JSON_OPTIONS));
// The text around a passage which the provenance store keeps:
yield return Case(new(typeof(IReadOnlyList<string>), SqliteProvenanceStoreClientImplementation.JSON_OPTIONS));
yield return Case(new(typeof(List<string>), SqliteProvenanceStoreClientImplementation.JSON_OPTIONS));
}
private static TestCaseData Case(JsonTestRoot root)
{
//
// The same System.Text.Json does with the first call of Serialize or Deserialize. Only then, the
// options hand out contracts:
//
root.Options.MakeReadOnly(populateMissingResolver: true);
return new TestCaseData(root).SetArgDisplayNames(root.ToString());
}
private static IEnumerable<Type> ManagedSettingValueTypes()
{
//
// Creating the settings registers every managed setting, just like the start of the app does:
//
_ = new Data();
return ManagedConfiguration.RegisteredSettings
.Select(setting => setting.GetType().GetGenericArguments()[1])
.Distinct()
.OrderBy(JsonContractDescription.TypeName, StringComparer.Ordinal);
}
}
+43
View File
@@ -0,0 +1,43 @@
using System.Text.Json;
namespace AIStudio.Tests.Serialization;
/// <summary>
/// Collects every type System.Text.Json meets while it reads or writes a root.
/// </summary>
public static class JsonTypeGraph
{
/// <summary>
/// Returns the root and every type it holds: the types of its properties, the keys and elements of its
/// collections, and its derived types, each of them once and in the order they were met.
/// </summary>
/// <param name="root">The type to start with.</param>
/// <param name="options">The options whose contracts tell what a type holds.</param>
public static IReadOnlyList<Type> Collect(Type root, JsonSerializerOptions options)
{
var types = new List<Type>();
Visit(root, options, [], types);
return types;
}
private static void Visit(Type type, JsonSerializerOptions options, HashSet<Type> visited, List<Type> types)
{
if (!visited.Add(type))
return;
types.Add(type);
var typeInfo = options.GetTypeInfo(type);
foreach (var property in typeInfo.Properties)
Visit(property.PropertyType, options, visited, types);
if (typeInfo.KeyType is { } keyType)
Visit(keyType, options, visited, types);
if (typeInfo.ElementType is { } elementType)
Visit(elementType, options, visited, types);
if (typeInfo.PolymorphismOptions is { } polymorphism)
foreach (var derivedType in polymorphism.DerivedTypes)
Visit(derivedType.DerivedType, options, visited, types);
}
}
+10 -1
View File
@@ -12,7 +12,16 @@ namespace AIStudio.Tests;
public sealed class TestHost
{
[OneTimeSetUp]
public void PrepareStaticApplicationState()
public void PrepareStaticApplicationState() => PrepareStaticState();
/// <summary>
/// Fills the static state of the app which the tests need.
/// </summary>
/// <remarks>
/// NUnit builds the cases of a TestCaseSource while it discovers the tests, before this fixture runs.
/// A source which touches a type of the app with a static logger calls this first.
/// </remarks>
public static void PrepareStaticState()
{
//
// A number of types in the app hold a static logger field that is initialized from