Actually commit the oAITests target wiring in project.pbxproj
The oAITests PBXNativeTarget (PBXContainerItemProxy, PBXTargetDependency,
XCBuildConfiguration with TEST_HOST/BUNDLE_LOADER, the "recommended
settings" changes accepted when the target was created -- DEAD_CODE_STRIPPING,
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED, STRING_CATALOG_GENERATE_SYMBOLS,
DEVELOPMENT_TEAM moved to project-level inheritance) was somehow never
actually staged in the very first "Add real oAITests target" commit
(8c7fb59) despite every xcodebuild test run since then depending on it
being present on disk. Every subsequent commit this session only staged
specific file paths (never oAI.xcodeproj again), so the gap went
unnoticed until a full `git status` review here.
Without this, anyone else pulling the branch (or a truly clean checkout
on this machine) would have all the .swift test files but no target to
compile them into -- xcodebuild test would fail to find oAITests at all.
Confirmed the diff is exactly the expected target-wiring content, nothing
unrelated or corrupted, before committing.
This commit is contained in:
@@ -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.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if conversation should be auto-saved based on criteria
|
/// Pure auto-save eligibility check, pulled out of `shouldAutoSave()` so the criteria
|
||||||
func shouldAutoSave() -> Bool {
|
/// (enabled, configured, cloned, min message count, not-already-saved) can be tested
|
||||||
// Check if auto-save is enabled
|
/// without a live ChatViewModel/GitSyncService/SettingsService.
|
||||||
guard settings.syncEnabled && settings.syncAutoSave else {
|
nonisolated static func shouldAutoSave(
|
||||||
return false
|
syncEnabled: Bool,
|
||||||
}
|
syncAutoSave: Bool,
|
||||||
|
syncConfigured: Bool,
|
||||||
// Check if sync is configured
|
isCloned: Bool,
|
||||||
guard settings.syncConfigured else {
|
chatMessageCount: Int,
|
||||||
return false
|
minMessages: Int,
|
||||||
}
|
lastSavedConversationId: String?,
|
||||||
|
currentConversationHash: String
|
||||||
// Check if repository is cloned
|
) -> Bool {
|
||||||
guard GitSyncService.shared.syncStatus.isCloned else {
|
guard syncEnabled && syncAutoSave else { return false }
|
||||||
return false
|
guard syncConfigured else { return false }
|
||||||
}
|
guard isCloned else { return false }
|
||||||
|
guard chatMessageCount >= minMessages else { return false }
|
||||||
// Check minimum message count
|
if let lastSavedId = lastSavedConversationId, lastSavedId == currentConversationHash {
|
||||||
let chatMessages = messages.filter { $0.role == .user || $0.role == .assistant }
|
|
||||||
guard chatMessages.count >= settings.syncAutoSaveMinMessages else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if already auto-saved this conversation
|
|
||||||
// (prevent duplicate saves)
|
|
||||||
if let lastSavedId = settings.syncLastAutoSaveConversationId {
|
|
||||||
// Create a hash of current conversation to detect if it's the same one
|
|
||||||
let currentHash = chatMessages.map { $0.content }.joined()
|
|
||||||
if lastSavedId == currentHash {
|
|
||||||
return false // Already saved this exact conversation
|
return false // Already saved this exact conversation
|
||||||
}
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
/// Check if conversation should be auto-saved based on criteria
|
||||||
|
func shouldAutoSave() -> Bool {
|
||||||
|
let chatMessages = messages.filter { $0.role == .user || $0.role == .assistant }
|
||||||
|
let currentHash = chatMessages.map { $0.content }.joined()
|
||||||
|
return Self.shouldAutoSave(
|
||||||
|
syncEnabled: settings.syncEnabled,
|
||||||
|
syncAutoSave: settings.syncAutoSave,
|
||||||
|
syncConfigured: settings.syncConfigured,
|
||||||
|
isCloned: GitSyncService.shared.syncStatus.isCloned,
|
||||||
|
chatMessageCount: chatMessages.count,
|
||||||
|
minMessages: settings.syncAutoSaveMinMessages,
|
||||||
|
lastSavedConversationId: settings.syncLastAutoSaveConversationId,
|
||||||
|
currentConversationHash: currentHash
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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