2.4.3 #9
@@ -11,8 +11,19 @@
|
|||||||
A550A8342F3C5C9300136F2B /* GRDB in Frameworks */ = {isa = PBXBuildFile; productRef = A550A6812F3B730000136F2B /* GRDB */; };
|
A550A8342F3C5C9300136F2B /* GRDB in Frameworks */ = {isa = PBXBuildFile; productRef = A550A6812F3B730000136F2B /* GRDB */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
|
/* Begin PBXContainerItemProxy section */
|
||||||
|
A586FF5530122589002CFF95 /* PBXContainerItemProxy */ = {
|
||||||
|
isa = PBXContainerItemProxy;
|
||||||
|
containerPortal = A550A65A2F3B72EA00136F2B /* Project object */;
|
||||||
|
proxyType = 1;
|
||||||
|
remoteGlobalIDString = A550A6612F3B72EA00136F2B;
|
||||||
|
remoteInfo = oAI;
|
||||||
|
};
|
||||||
|
/* End PBXContainerItemProxy section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
A550A6622F3B72EA00136F2B /* oAI.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = oAI.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
A550A6622F3B72EA00136F2B /* oAI.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = oAI.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
A586FF5130122589002CFF95 /* oAITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = oAITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||||
@@ -34,6 +45,11 @@
|
|||||||
path = oAI;
|
path = oAI;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
A586FF5230122589002CFF95 /* oAITests */ = {
|
||||||
|
isa = PBXFileSystemSynchronizedRootGroup;
|
||||||
|
path = oAITests;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
/* End PBXFileSystemSynchronizedRootGroup section */
|
/* End PBXFileSystemSynchronizedRootGroup section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
@@ -46,6 +62,13 @@
|
|||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
A586FF4E30122589002CFF95 /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
/* End PBXFrameworksBuildPhase section */
|
/* End PBXFrameworksBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXGroup section */
|
/* Begin PBXGroup section */
|
||||||
@@ -53,6 +76,7 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
A550A6642F3B72EA00136F2B /* oAI */,
|
A550A6642F3B72EA00136F2B /* oAI */,
|
||||||
|
A586FF5230122589002CFF95 /* oAITests */,
|
||||||
A550A6632F3B72EA00136F2B /* Products */,
|
A550A6632F3B72EA00136F2B /* Products */,
|
||||||
);
|
);
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -61,6 +85,7 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
A550A6622F3B72EA00136F2B /* oAI.app */,
|
A550A6622F3B72EA00136F2B /* oAI.app */,
|
||||||
|
A586FF5130122589002CFF95 /* oAITests.xctest */,
|
||||||
);
|
);
|
||||||
name = Products;
|
name = Products;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -92,6 +117,29 @@
|
|||||||
productReference = A550A6622F3B72EA00136F2B /* oAI.app */;
|
productReference = A550A6622F3B72EA00136F2B /* oAI.app */;
|
||||||
productType = "com.apple.product-type.application";
|
productType = "com.apple.product-type.application";
|
||||||
};
|
};
|
||||||
|
A586FF5030122589002CFF95 /* oAITests */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = A586FF5930122589002CFF95 /* Build configuration list for PBXNativeTarget "oAITests" */;
|
||||||
|
buildPhases = (
|
||||||
|
A586FF4D30122589002CFF95 /* Sources */,
|
||||||
|
A586FF4E30122589002CFF95 /* Frameworks */,
|
||||||
|
A586FF4F30122589002CFF95 /* Resources */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
A586FF5630122589002CFF95 /* PBXTargetDependency */,
|
||||||
|
);
|
||||||
|
fileSystemSynchronizedGroups = (
|
||||||
|
A586FF5230122589002CFF95 /* oAITests */,
|
||||||
|
);
|
||||||
|
name = oAITests;
|
||||||
|
packageProductDependencies = (
|
||||||
|
);
|
||||||
|
productName = oAITests;
|
||||||
|
productReference = A586FF5130122589002CFF95 /* oAITests.xctest */;
|
||||||
|
productType = "com.apple.product-type.bundle.unit-test";
|
||||||
|
};
|
||||||
/* End PBXNativeTarget section */
|
/* End PBXNativeTarget section */
|
||||||
|
|
||||||
/* Begin PBXProject section */
|
/* Begin PBXProject section */
|
||||||
@@ -99,12 +147,16 @@
|
|||||||
isa = PBXProject;
|
isa = PBXProject;
|
||||||
attributes = {
|
attributes = {
|
||||||
BuildIndependentTargetsInParallel = 1;
|
BuildIndependentTargetsInParallel = 1;
|
||||||
LastSwiftUpdateCheck = 2620;
|
LastSwiftUpdateCheck = 2700;
|
||||||
LastUpgradeCheck = 2620;
|
LastUpgradeCheck = 2700;
|
||||||
TargetAttributes = {
|
TargetAttributes = {
|
||||||
A550A6612F3B72EA00136F2B = {
|
A550A6612F3B72EA00136F2B = {
|
||||||
CreatedOnToolsVersion = 26.2;
|
CreatedOnToolsVersion = 26.2;
|
||||||
};
|
};
|
||||||
|
A586FF5030122589002CFF95 = {
|
||||||
|
CreatedOnToolsVersion = 27.0;
|
||||||
|
TestTargetID = A550A6612F3B72EA00136F2B;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
buildConfigurationList = A550A65D2F3B72EA00136F2B /* Build configuration list for PBXProject "oAI" */;
|
buildConfigurationList = A550A65D2F3B72EA00136F2B /* Build configuration list for PBXProject "oAI" */;
|
||||||
@@ -131,6 +183,7 @@
|
|||||||
projectRoot = "";
|
projectRoot = "";
|
||||||
targets = (
|
targets = (
|
||||||
A550A6612F3B72EA00136F2B /* oAI */,
|
A550A6612F3B72EA00136F2B /* oAI */,
|
||||||
|
A586FF5030122589002CFF95 /* oAITests */,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
/* End PBXProject section */
|
/* End PBXProject section */
|
||||||
@@ -143,6 +196,13 @@
|
|||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
A586FF4F30122589002CFF95 /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
/* End PBXResourcesBuildPhase section */
|
/* End PBXResourcesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXSourcesBuildPhase section */
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
@@ -153,14 +213,30 @@
|
|||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
A586FF4D30122589002CFF95 /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
/* End PBXSourcesBuildPhase section */
|
/* End PBXSourcesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXTargetDependency section */
|
||||||
|
A586FF5630122589002CFF95 /* PBXTargetDependency */ = {
|
||||||
|
isa = PBXTargetDependency;
|
||||||
|
target = A550A6612F3B72EA00136F2B /* oAI */;
|
||||||
|
targetProxy = A586FF5530122589002CFF95 /* PBXContainerItemProxy */;
|
||||||
|
};
|
||||||
|
/* End PBXTargetDependency section */
|
||||||
|
|
||||||
/* Begin XCBuildConfiguration section */
|
/* Begin XCBuildConfiguration section */
|
||||||
A550A66B2F3B72EC00136F2B /* Debug */ = {
|
A550A66B2F3B72EC00136F2B /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||||
|
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
|
||||||
CLANG_ANALYZER_NONNULL = YES;
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||||
@@ -190,7 +266,9 @@
|
|||||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
COPY_PHASE_STRIP = NO;
|
COPY_PHASE_STRIP = NO;
|
||||||
|
DEAD_CODE_STRIPPING = YES;
|
||||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||||
|
DEVELOPMENT_TEAM = 6RJQ2QZYPG;
|
||||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
ENABLE_TESTABILITY = YES;
|
ENABLE_TESTABILITY = YES;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
@@ -212,6 +290,7 @@
|
|||||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
ONLY_ACTIVE_ARCH = YES;
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
|
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
};
|
};
|
||||||
@@ -222,6 +301,7 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||||
|
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
|
||||||
CLANG_ANALYZER_NONNULL = YES;
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||||
@@ -251,7 +331,9 @@
|
|||||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
COPY_PHASE_STRIP = NO;
|
COPY_PHASE_STRIP = NO;
|
||||||
|
DEAD_CODE_STRIPPING = YES;
|
||||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
|
DEVELOPMENT_TEAM = 6RJQ2QZYPG;
|
||||||
ENABLE_NS_ASSERTIONS = NO;
|
ENABLE_NS_ASSERTIONS = NO;
|
||||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
@@ -266,6 +348,7 @@
|
|||||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
|
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||||
SWIFT_COMPILATION_MODE = wholemodule;
|
SWIFT_COMPILATION_MODE = wholemodule;
|
||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
@@ -278,12 +361,12 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = oAI/oAI.entitlements;
|
CODE_SIGN_ENTITLEMENTS = oAI/oAI.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEVELOPMENT_TEAM = 6RJQ2QZYPG;
|
DEAD_CODE_STRIPPING = YES;
|
||||||
ENABLE_APP_SANDBOX = NO;
|
ENABLE_APP_SANDBOX = NO;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
ENABLE_USER_SELECTED_FILES = readonly;
|
ENABLE_USER_SELECTED_FILES = readonly;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = "oAI/Info.plist";
|
INFOPLIST_FILE = oAI/Info.plist;
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
|
||||||
INFOPLIST_KEY_NSCalendarsFullAccessUsageDescription = "oAI can read and create calendar events when you ask it to, if you enable Calendar access in Settings.";
|
INFOPLIST_KEY_NSCalendarsFullAccessUsageDescription = "oAI can read and create calendar events when you ask it to, if you enable Calendar access in Settings.";
|
||||||
INFOPLIST_KEY_NSContactsUsageDescription = "oAI can search your contacts when you ask it to, if you enable Contacts access in Settings.";
|
INFOPLIST_KEY_NSContactsUsageDescription = "oAI can search your contacts when you ask it to, if you enable Contacts access in Settings.";
|
||||||
@@ -328,12 +411,12 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = oAI/oAI.entitlements;
|
CODE_SIGN_ENTITLEMENTS = oAI/oAI.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEVELOPMENT_TEAM = 6RJQ2QZYPG;
|
DEAD_CODE_STRIPPING = YES;
|
||||||
ENABLE_APP_SANDBOX = NO;
|
ENABLE_APP_SANDBOX = NO;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
ENABLE_USER_SELECTED_FILES = readonly;
|
ENABLE_USER_SELECTED_FILES = readonly;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = "oAI/Info.plist";
|
INFOPLIST_FILE = oAI/Info.plist;
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
|
||||||
INFOPLIST_KEY_NSCalendarsFullAccessUsageDescription = "oAI can read and create calendar events when you ask it to, if you enable Calendar access in Settings.";
|
INFOPLIST_KEY_NSCalendarsFullAccessUsageDescription = "oAI can read and create calendar events when you ask it to, if you enable Calendar access in Settings.";
|
||||||
INFOPLIST_KEY_NSContactsUsageDescription = "oAI can search your contacts when you ask it to, if you enable Contacts access in Settings.";
|
INFOPLIST_KEY_NSContactsUsageDescription = "oAI can search your contacts when you ask it to, if you enable Contacts access in Settings.";
|
||||||
@@ -370,6 +453,50 @@
|
|||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
};
|
};
|
||||||
|
A586FF5730122589002CFF95 /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
DEAD_CODE_STRIPPING = YES;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
MACOSX_DEPLOYMENT_TARGET = 27.0;
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.oai.oAITests;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SDKROOT = macosx;
|
||||||
|
STRING_CATALOG_GENERATE_SYMBOLS = NO;
|
||||||
|
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||||
|
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/oAI.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/oAI";
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
A586FF5830122589002CFF95 /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
DEAD_CODE_STRIPPING = YES;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
MACOSX_DEPLOYMENT_TARGET = 27.0;
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.oai.oAITests;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SDKROOT = macosx;
|
||||||
|
STRING_CATALOG_GENERATE_SYMBOLS = NO;
|
||||||
|
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||||
|
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/oAI.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/oAI";
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
/* End XCBuildConfiguration section */
|
/* End XCBuildConfiguration section */
|
||||||
|
|
||||||
/* Begin XCConfigurationList section */
|
/* Begin XCConfigurationList section */
|
||||||
@@ -391,6 +518,15 @@
|
|||||||
defaultConfigurationIsVisible = 0;
|
defaultConfigurationIsVisible = 0;
|
||||||
defaultConfigurationName = Release;
|
defaultConfigurationName = Release;
|
||||||
};
|
};
|
||||||
|
A586FF5930122589002CFF95 /* Build configuration list for PBXNativeTarget "oAITests" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
A586FF5730122589002CFF95 /* Debug */,
|
||||||
|
A586FF5830122589002CFF95 /* Release */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
/* End XCConfigurationList section */
|
/* End XCConfigurationList section */
|
||||||
|
|
||||||
/* Begin XCRemoteSwiftPackageReference section */
|
/* Begin XCRemoteSwiftPackageReference section */
|
||||||
|
|||||||
@@ -186,6 +186,21 @@ class AnthropicProvider: AIProvider {
|
|||||||
("claude-haiku", 0.80, 4.0),
|
("claude-haiku", 0.80, 4.0),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
/// Fuzzy pricing lookup for a model ID not found in `knownModels`: finds the
|
||||||
|
/// longest matching prefix in `fallback` (longest match wins when several
|
||||||
|
/// prefixes match, e.g. a future "claude-sonnet-mini" against both
|
||||||
|
/// "claude-sonnet" and a shorter unrelated prefix). Unmatched IDs price at zero.
|
||||||
|
static func resolveFallbackPricing(
|
||||||
|
for modelId: String,
|
||||||
|
fallback: [(prefix: String, prompt: Double, completion: Double)] = pricingFallback
|
||||||
|
) -> ModelInfo.Pricing {
|
||||||
|
let match = fallback
|
||||||
|
.filter { modelId.hasPrefix($0.prefix) }
|
||||||
|
.max(by: { $0.prefix.count < $1.prefix.count })
|
||||||
|
return match.map { ModelInfo.Pricing(prompt: $0.prompt, completion: $0.completion) }
|
||||||
|
?? ModelInfo.Pricing(prompt: 0, completion: 0)
|
||||||
|
}
|
||||||
|
|
||||||
/// Fetch live model list from GET /v1/models, enriched with local pricing/context metadata.
|
/// Fetch live model list from GET /v1/models, enriched with local pricing/context metadata.
|
||||||
/// Falls back to knownModels if the request fails (no key, offline, etc.).
|
/// Falls back to knownModels if the request fails (no key, offline, etc.).
|
||||||
func listModels() async throws -> [ModelInfo] {
|
func listModels() async throws -> [ModelInfo] {
|
||||||
@@ -223,11 +238,7 @@ class AnthropicProvider: AIProvider {
|
|||||||
// Exact match first
|
// Exact match first
|
||||||
if let known = enrichment[id] { return known }
|
if let known = enrichment[id] { return known }
|
||||||
// Fuzzy fallback: find the longest prefix that matches
|
// Fuzzy fallback: find the longest prefix that matches
|
||||||
let fallback = Self.pricingFallback
|
let pricing = Self.resolveFallbackPricing(for: id)
|
||||||
.filter { id.hasPrefix($0.prefix) }
|
|
||||||
.max(by: { $0.prefix.count < $1.prefix.count })
|
|
||||||
let pricing = fallback.map { ModelInfo.Pricing(prompt: $0.prompt, completion: $0.completion) }
|
|
||||||
?? ModelInfo.Pricing(prompt: 0, completion: 0)
|
|
||||||
return ModelInfo(
|
return ModelInfo(
|
||||||
id: id,
|
id: id,
|
||||||
name: displayName,
|
name: displayName,
|
||||||
|
|||||||
@@ -206,37 +206,68 @@ final class BackupService {
|
|||||||
log.debug("Pushed \(payload.ids.count) favorite model(s) to iCloud")
|
log.debug("Pushed \(payload.ids.count) favorite model(s) to iCloud")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Outcome of reconciling local favorites against the iCloud copy. Pure result type --
|
||||||
|
/// see `decideFavoritesSync` for the actual decision logic.
|
||||||
|
enum FavoritesSyncDecision: Equatable {
|
||||||
|
case applyRemote(ids: Set<String>, updatedAt: String)
|
||||||
|
case pushLocal
|
||||||
|
case mergeAndPush(ids: Set<String>, updatedAt: String)
|
||||||
|
case noop
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Last-write-wins reconciliation, pulled out of `syncFavoritesOnLaunch` so the branching
|
||||||
|
/// logic (5 cases: remote absent / remote newer / local newer / tied-but-different /
|
||||||
|
/// tied-and-identical) can be tested without touching iCloud Drive or SettingsService.
|
||||||
|
static func decideFavoritesSync(
|
||||||
|
localIds: Set<String>,
|
||||||
|
localUpdatedAt: String,
|
||||||
|
remote: FavoritesPayload?,
|
||||||
|
now: Date
|
||||||
|
) -> FavoritesSyncDecision {
|
||||||
|
guard let remote else {
|
||||||
|
return .pushLocal
|
||||||
|
}
|
||||||
|
if remote.updatedAt > localUpdatedAt {
|
||||||
|
return .applyRemote(ids: Set(remote.ids), updatedAt: remote.updatedAt)
|
||||||
|
} else if localUpdatedAt > remote.updatedAt {
|
||||||
|
return .pushLocal
|
||||||
|
} else if Set(remote.ids) != localIds {
|
||||||
|
// Tied timestamps (both empty is the common case: two machines that already had
|
||||||
|
// favorites before this sync feature existed, neither ever bumped the timestamp).
|
||||||
|
// Union rather than silently dropping one side's favorites.
|
||||||
|
let merged = localIds.union(remote.ids)
|
||||||
|
return .mergeAndPush(ids: merged, updatedAt: ISO8601DateFormatter().string(from: now))
|
||||||
|
}
|
||||||
|
return .noop
|
||||||
|
}
|
||||||
|
|
||||||
/// Reconcile local favorites with the iCloud copy using last-write-wins (by timestamp).
|
/// Reconcile local favorites with the iCloud copy using last-write-wins (by timestamp).
|
||||||
/// Call on launch (and optionally on app-become-active) to pick up changes from other machines.
|
/// Call on launch (and optionally on app-become-active) to pick up changes from other machines.
|
||||||
func syncFavoritesOnLaunch() async {
|
func syncFavoritesOnLaunch() async {
|
||||||
let settings = SettingsService.shared
|
let settings = SettingsService.shared
|
||||||
let fileURL = favoritesFileURL()
|
let fileURL = favoritesFileURL()
|
||||||
|
|
||||||
guard let data = try? Data(contentsOf: fileURL),
|
let remote: FavoritesPayload? = {
|
||||||
let remote = try? JSONDecoder().decode(FavoritesPayload.self, from: data) else {
|
guard let data = try? Data(contentsOf: fileURL) else { return nil }
|
||||||
// No remote copy yet — push local state (may be empty, that's fine).
|
return try? JSONDecoder().decode(FavoritesPayload.self, from: data)
|
||||||
await pushFavorites()
|
}()
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let localUpdatedAt = settings.favoriteModelsUpdatedAt
|
|
||||||
let localIds = settings.favoriteModelIds
|
let localIds = settings.favoriteModelIds
|
||||||
if remote.updatedAt > localUpdatedAt {
|
|
||||||
settings.favoriteModelIds = Set(remote.ids)
|
switch Self.decideFavoritesSync(localIds: localIds, localUpdatedAt: settings.favoriteModelsUpdatedAt, remote: remote, now: Date()) {
|
||||||
settings.favoriteModelsUpdatedAt = remote.updatedAt
|
case .pushLocal:
|
||||||
log.info("Applied \(remote.ids.count) favorite model(s) from iCloud (remote was newer)")
|
|
||||||
} else if localUpdatedAt > remote.updatedAt {
|
|
||||||
await pushFavorites()
|
await pushFavorites()
|
||||||
} else if Set(remote.ids) != localIds {
|
case .applyRemote(let ids, let updatedAt):
|
||||||
// Tied timestamps (both empty is the common case: two machines that already had
|
settings.favoriteModelIds = ids
|
||||||
// favorites before this sync feature existed, neither ever bumped the timestamp).
|
settings.favoriteModelsUpdatedAt = updatedAt
|
||||||
// Union rather than silently dropping one side's favorites.
|
log.info("Applied \(ids.count) favorite model(s) from iCloud (remote was newer)")
|
||||||
settings.favoriteModelIds = localIds.union(remote.ids)
|
case .mergeAndPush(let ids, let updatedAt):
|
||||||
settings.favoriteModelsUpdatedAt = ISO8601DateFormatter().string(from: Date())
|
settings.favoriteModelIds = ids
|
||||||
|
settings.favoriteModelsUpdatedAt = updatedAt
|
||||||
await pushFavorites()
|
await pushFavorites()
|
||||||
log.info("Merged favorites from iCloud (tied timestamps) — union of \(localIds.count) local + \(remote.ids.count) remote")
|
log.info("Merged favorites from iCloud (tied timestamps) — union of \(localIds.count) local + \(remote?.ids.count ?? 0) remote")
|
||||||
|
case .noop:
|
||||||
|
break
|
||||||
}
|
}
|
||||||
// Equal timestamps and identical sets: already in sync, nothing to do.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Automatic Backup
|
// MARK: - Automatic Backup
|
||||||
@@ -252,17 +283,27 @@ final class BackupService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkAndPerformAutoBackupIfDue() async {
|
/// Whether an automatic backup should run now, given the chosen frequency and the last
|
||||||
let frequency = SettingsService.shared.autoBackupFrequency
|
/// known backup time. Pulled out of `checkAndPerformAutoBackupIfDue` for testability --
|
||||||
|
/// "manual" is never due, a missing last-backup-date is always due, otherwise it's due
|
||||||
|
/// once the elapsed time reaches the frequency's interval.
|
||||||
|
static func isBackupDue(frequency: String, lastBackupDate: Date?, now: Date) -> Bool {
|
||||||
let interval: TimeInterval
|
let interval: TimeInterval
|
||||||
switch frequency {
|
switch frequency {
|
||||||
case "daily": interval = Self.dailyInterval
|
case "daily": interval = dailyInterval
|
||||||
case "weekly": interval = Self.weeklyInterval
|
case "weekly": interval = weeklyInterval
|
||||||
default: return // "manual" — user triggers backups by hand
|
default: return false // "manual" — user triggers backups by hand
|
||||||
}
|
}
|
||||||
|
guard let last = lastBackupDate else { return true }
|
||||||
|
return now.timeIntervalSince(last) >= interval
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkAndPerformAutoBackupIfDue() async {
|
||||||
|
let frequency = SettingsService.shared.autoBackupFrequency
|
||||||
|
guard frequency == "daily" || frequency == "weekly" else { return }
|
||||||
|
|
||||||
checkForExistingBackup()
|
checkForExistingBackup()
|
||||||
if let last = lastBackupDate, Date().timeIntervalSince(last) < interval {
|
guard Self.isBackupDue(frequency: frequency, lastBackupDate: lastBackupDate, now: Date()) else {
|
||||||
return // not due yet
|
return // not due yet
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ class GitSyncService {
|
|||||||
func testConnection() async throws -> String {
|
func testConnection() async throws -> String {
|
||||||
let url = try buildAuthenticatedURL()
|
let url = try buildAuthenticatedURL()
|
||||||
_ = try await runGit(["ls-remote", url])
|
_ = try await runGit(["ls-remote", url])
|
||||||
return "Connected to \(extractProvider())"
|
return "Connected to \(Self.extractProvider(from: settings.syncRepoURL))"
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clone repository to local path
|
/// Clone repository to local path
|
||||||
@@ -703,8 +703,7 @@ class GitSyncService {
|
|||||||
return name.components(separatedBy: invalid).joined(separator: "-")
|
return name.components(separatedBy: invalid).joined(separator: "-")
|
||||||
}
|
}
|
||||||
|
|
||||||
private func extractProvider() -> String {
|
static func extractProvider(from url: String) -> String {
|
||||||
let url = settings.syncRepoURL
|
|
||||||
if url.contains("github.com") {
|
if url.contains("github.com") {
|
||||||
return "GitHub"
|
return "GitHub"
|
||||||
} else if url.contains("gitlab.com") {
|
} else if url.contains("gitlab.com") {
|
||||||
|
|||||||
@@ -1888,40 +1888,43 @@ Don't narrate future actions ("Let me...") - just use the tools.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Pure auto-save eligibility check, pulled out of `shouldAutoSave()` so the criteria
|
||||||
|
/// (enabled, configured, cloned, min message count, not-already-saved) can be tested
|
||||||
|
/// without a live ChatViewModel/GitSyncService/SettingsService.
|
||||||
|
nonisolated static func shouldAutoSave(
|
||||||
|
syncEnabled: Bool,
|
||||||
|
syncAutoSave: Bool,
|
||||||
|
syncConfigured: Bool,
|
||||||
|
isCloned: Bool,
|
||||||
|
chatMessageCount: Int,
|
||||||
|
minMessages: Int,
|
||||||
|
lastSavedConversationId: String?,
|
||||||
|
currentConversationHash: String
|
||||||
|
) -> Bool {
|
||||||
|
guard syncEnabled && syncAutoSave else { return false }
|
||||||
|
guard syncConfigured else { return false }
|
||||||
|
guard isCloned else { return false }
|
||||||
|
guard chatMessageCount >= minMessages else { return false }
|
||||||
|
if let lastSavedId = lastSavedConversationId, lastSavedId == currentConversationHash {
|
||||||
|
return false // Already saved this exact conversation
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
/// Check if conversation should be auto-saved based on criteria
|
/// Check if conversation should be auto-saved based on criteria
|
||||||
func shouldAutoSave() -> Bool {
|
func shouldAutoSave() -> Bool {
|
||||||
// Check if auto-save is enabled
|
|
||||||
guard settings.syncEnabled && settings.syncAutoSave else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if sync is configured
|
|
||||||
guard settings.syncConfigured else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if repository is cloned
|
|
||||||
guard GitSyncService.shared.syncStatus.isCloned else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check minimum message count
|
|
||||||
let chatMessages = messages.filter { $0.role == .user || $0.role == .assistant }
|
let chatMessages = messages.filter { $0.role == .user || $0.role == .assistant }
|
||||||
guard chatMessages.count >= settings.syncAutoSaveMinMessages else {
|
let currentHash = chatMessages.map { $0.content }.joined()
|
||||||
return false
|
return Self.shouldAutoSave(
|
||||||
}
|
syncEnabled: settings.syncEnabled,
|
||||||
|
syncAutoSave: settings.syncAutoSave,
|
||||||
// Check if already auto-saved this conversation
|
syncConfigured: settings.syncConfigured,
|
||||||
// (prevent duplicate saves)
|
isCloned: GitSyncService.shared.syncStatus.isCloned,
|
||||||
if let lastSavedId = settings.syncLastAutoSaveConversationId {
|
chatMessageCount: chatMessages.count,
|
||||||
// Create a hash of current conversation to detect if it's the same one
|
minMessages: settings.syncAutoSaveMinMessages,
|
||||||
let currentHash = chatMessages.map { $0.content }.joined()
|
lastSavedConversationId: settings.syncLastAutoSaveConversationId,
|
||||||
if lastSavedId == currentHash {
|
currentConversationHash: currentHash
|
||||||
return false // Already saved this exact conversation
|
)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Auto-save the current conversation with background summarization
|
/// Auto-save the current conversation with background summarization
|
||||||
|
|||||||
@@ -177,4 +177,31 @@ struct AnthropicProviderTests {
|
|||||||
#expect(items?["type"] as? String == "string")
|
#expect(items?["type"] as? String == "string")
|
||||||
#expect(dict["required"] == nil)
|
#expect(dict["required"] == nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - resolveFallbackPricing
|
||||||
|
|
||||||
|
@Test("Exact-length single prefix match returns that tier's pricing")
|
||||||
|
func fallbackPricingSingleMatch() {
|
||||||
|
let pricing = AnthropicProvider.resolveFallbackPricing(for: "claude-haiku-4-5-20251001")
|
||||||
|
#expect(pricing.prompt == 0.80)
|
||||||
|
#expect(pricing.completion == 4.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Longest matching prefix wins when multiple prefixes could match")
|
||||||
|
func fallbackPricingLongestPrefixWins() {
|
||||||
|
let fallback: [(prefix: String, prompt: Double, completion: Double)] = [
|
||||||
|
("claude", 1.0, 2.0),
|
||||||
|
("claude-sonnet", 3.0, 15.0),
|
||||||
|
]
|
||||||
|
let pricing = AnthropicProvider.resolveFallbackPricing(for: "claude-sonnet-5", fallback: fallback)
|
||||||
|
#expect(pricing.prompt == 3.0)
|
||||||
|
#expect(pricing.completion == 15.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("An unmatched model ID prices at zero rather than guessing")
|
||||||
|
func fallbackPricingNoMatch() {
|
||||||
|
let pricing = AnthropicProvider.resolveFallbackPricing(for: "some-future-unknown-model")
|
||||||
|
#expect(pricing.prompt == 0)
|
||||||
|
#expect(pricing.completion == 0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
//
|
||||||
|
// BackupServiceTests.swift
|
||||||
|
// oAITests
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
|
||||||
|
// Copyright (C) 2026 Rune Olsen
|
||||||
|
|
||||||
|
import Testing
|
||||||
|
import Foundation
|
||||||
|
@testable import oAI
|
||||||
|
|
||||||
|
@Suite("BackupService.decideFavoritesSync")
|
||||||
|
struct BackupServiceFavoritesSyncTests {
|
||||||
|
|
||||||
|
private let now = Date(timeIntervalSince1970: 1_700_000_000)
|
||||||
|
|
||||||
|
@Test("No remote copy yet -- push local state")
|
||||||
|
func noRemoteCopy() {
|
||||||
|
let decision = BackupService.decideFavoritesSync(localIds: ["a", "b"], localUpdatedAt: "", remote: nil, now: now)
|
||||||
|
#expect(decision == .pushLocal)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Remote is newer -- apply it locally")
|
||||||
|
func remoteNewer() {
|
||||||
|
let remote = FavoritesPayload(updatedAt: "2026-02-01T00:00:00Z", ids: ["x", "y"])
|
||||||
|
let decision = BackupService.decideFavoritesSync(
|
||||||
|
localIds: ["a"],
|
||||||
|
localUpdatedAt: "2026-01-01T00:00:00Z",
|
||||||
|
remote: remote,
|
||||||
|
now: now
|
||||||
|
)
|
||||||
|
#expect(decision == .applyRemote(ids: ["x", "y"], updatedAt: "2026-02-01T00:00:00Z"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Local is newer -- push local state, don't touch remote")
|
||||||
|
func localNewer() {
|
||||||
|
let remote = FavoritesPayload(updatedAt: "2026-01-01T00:00:00Z", ids: ["x"])
|
||||||
|
let decision = BackupService.decideFavoritesSync(
|
||||||
|
localIds: ["a"],
|
||||||
|
localUpdatedAt: "2026-02-01T00:00:00Z",
|
||||||
|
remote: remote,
|
||||||
|
now: now
|
||||||
|
)
|
||||||
|
#expect(decision == .pushLocal)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Tied timestamps with different sets -- merge via union and push")
|
||||||
|
func tiedTimestampsDifferentSets() {
|
||||||
|
let remote = FavoritesPayload(updatedAt: "", ids: ["b", "c"])
|
||||||
|
let decision = BackupService.decideFavoritesSync(localIds: ["a", "b"], localUpdatedAt: "", remote: remote, now: now)
|
||||||
|
guard case .mergeAndPush(let ids, let updatedAt) = decision else {
|
||||||
|
Issue.record("Expected .mergeAndPush")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
#expect(ids == ["a", "b", "c"])
|
||||||
|
#expect(!updatedAt.isEmpty) // stamped with `now`, not left blank
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Tied timestamps with identical sets -- no-op, already in sync")
|
||||||
|
func tiedTimestampsIdenticalSets() {
|
||||||
|
let remote = FavoritesPayload(updatedAt: "2026-01-01T00:00:00Z", ids: ["a", "b"])
|
||||||
|
let decision = BackupService.decideFavoritesSync(
|
||||||
|
localIds: ["a", "b"],
|
||||||
|
localUpdatedAt: "2026-01-01T00:00:00Z",
|
||||||
|
remote: remote,
|
||||||
|
now: now
|
||||||
|
)
|
||||||
|
#expect(decision == .noop)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suite("BackupService.isBackupDue")
|
||||||
|
struct BackupServiceAutoBackupTests {
|
||||||
|
|
||||||
|
private let now = Date(timeIntervalSince1970: 1_700_000_000)
|
||||||
|
private let oneHour: TimeInterval = 3600
|
||||||
|
|
||||||
|
@Test("Manual frequency is never due")
|
||||||
|
func manualNeverDue() {
|
||||||
|
#expect(!BackupService.isBackupDue(frequency: "manual", lastBackupDate: nil, now: now))
|
||||||
|
#expect(!BackupService.isBackupDue(frequency: "manual", lastBackupDate: now.addingTimeInterval(-1_000_000), now: now))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("No prior backup at all is always due, regardless of frequency")
|
||||||
|
func noPriorBackupIsAlwaysDue() {
|
||||||
|
#expect(BackupService.isBackupDue(frequency: "daily", lastBackupDate: nil, now: now))
|
||||||
|
#expect(BackupService.isBackupDue(frequency: "weekly", lastBackupDate: nil, now: now))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Daily: not due before 24 hours have passed, due at or after")
|
||||||
|
func dailyThreshold() {
|
||||||
|
let justUnder = now.addingTimeInterval(-(24 * oneHour - 1))
|
||||||
|
let exactly = now.addingTimeInterval(-(24 * oneHour))
|
||||||
|
#expect(!BackupService.isBackupDue(frequency: "daily", lastBackupDate: justUnder, now: now))
|
||||||
|
#expect(BackupService.isBackupDue(frequency: "daily", lastBackupDate: exactly, now: now))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Weekly: not due before 7 days have passed, due at or after")
|
||||||
|
func weeklyThreshold() {
|
||||||
|
let justUnder = now.addingTimeInterval(-(7 * 24 * oneHour - 1))
|
||||||
|
let exactly = now.addingTimeInterval(-(7 * 24 * oneHour))
|
||||||
|
#expect(!BackupService.isBackupDue(frequency: "weekly", lastBackupDate: justUnder, now: now))
|
||||||
|
#expect(BackupService.isBackupDue(frequency: "weekly", lastBackupDate: exactly, now: now))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -103,4 +103,60 @@ struct ChatViewModelPureLogicTests {
|
|||||||
let pricing = ModelInfo.Pricing(prompt: 3.0, completion: 15.0)
|
let pricing = ModelInfo.Pricing(prompt: 3.0, completion: 15.0)
|
||||||
#expect(ChatViewModel.calculateCost(usage: usage, pricing: pricing) == 0.0)
|
#expect(ChatViewModel.calculateCost(usage: usage, pricing: pricing) == 0.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - shouldAutoSave
|
||||||
|
|
||||||
|
private func autoSaveEligible(
|
||||||
|
syncEnabled: Bool = true,
|
||||||
|
syncAutoSave: Bool = true,
|
||||||
|
syncConfigured: Bool = true,
|
||||||
|
isCloned: Bool = true,
|
||||||
|
chatMessageCount: Int = 10,
|
||||||
|
minMessages: Int = 4,
|
||||||
|
lastSavedConversationId: String? = nil,
|
||||||
|
currentConversationHash: String = "hash-a"
|
||||||
|
) -> Bool {
|
||||||
|
ChatViewModel.shouldAutoSave(
|
||||||
|
syncEnabled: syncEnabled,
|
||||||
|
syncAutoSave: syncAutoSave,
|
||||||
|
syncConfigured: syncConfigured,
|
||||||
|
isCloned: isCloned,
|
||||||
|
chatMessageCount: chatMessageCount,
|
||||||
|
minMessages: minMessages,
|
||||||
|
lastSavedConversationId: lastSavedConversationId,
|
||||||
|
currentConversationHash: currentConversationHash
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Eligible when every criterion is satisfied")
|
||||||
|
func autoSaveEligibleWhenAllCriteriaMet() {
|
||||||
|
#expect(autoSaveEligible())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Not eligible when sync or auto-save is disabled")
|
||||||
|
func autoSaveIneligibleWhenDisabled() {
|
||||||
|
#expect(!autoSaveEligible(syncEnabled: false))
|
||||||
|
#expect(!autoSaveEligible(syncAutoSave: false))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Not eligible when sync isn't configured or the repo isn't cloned")
|
||||||
|
func autoSaveIneligibleWhenNotConfiguredOrCloned() {
|
||||||
|
#expect(!autoSaveEligible(syncConfigured: false))
|
||||||
|
#expect(!autoSaveEligible(isCloned: false))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Not eligible below the minimum message count")
|
||||||
|
func autoSaveIneligibleBelowMinimumMessages() {
|
||||||
|
#expect(!autoSaveEligible(chatMessageCount: 2, minMessages: 4))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Not eligible if this exact conversation was already auto-saved")
|
||||||
|
func autoSaveIneligibleWhenAlreadySaved() {
|
||||||
|
#expect(!autoSaveEligible(lastSavedConversationId: "hash-a", currentConversationHash: "hash-a"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Eligible when the last saved hash differs from the current conversation")
|
||||||
|
func autoSaveEligibleWhenConversationChanged() {
|
||||||
|
#expect(autoSaveEligible(lastSavedConversationId: "hash-a", currentConversationHash: "hash-b"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,4 +91,18 @@ struct GitSyncServiceTests {
|
|||||||
let shortKey = "sk-" + String(repeating: "a", count: 31)
|
let shortKey = "sk-" + String(repeating: "a", count: 31)
|
||||||
#expect(!service.detectSecretsInText(shortKey).contains("OpenAI Key"))
|
#expect(!service.detectSecretsInText(shortKey).contains("OpenAI Key"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - extractProvider
|
||||||
|
|
||||||
|
@Test("Recognizes github.com, gitlab.com, and gitea URLs by name")
|
||||||
|
func extractProviderRecognizesKnownHosts() {
|
||||||
|
#expect(GitSyncService.extractProvider(from: "https://github.com/user/repo.git") == "GitHub")
|
||||||
|
#expect(GitSyncService.extractProvider(from: "https://gitlab.com/user/repo.git") == "GitLab")
|
||||||
|
#expect(GitSyncService.extractProvider(from: "https://my-gitea-instance.example.com/user/repo.git") == "Gitea")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Falls back to a generic label for an unrecognized host")
|
||||||
|
func extractProviderFallsBackForUnknownHost() {
|
||||||
|
#expect(GitSyncService.extractProvider(from: "https://gitlab.pm/rune/oai-swift.git") == "Git repository")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user