feat(mobile): improve android edgeless & ci (#15118)
#### PR Dependency Tree * **PR #15118** 👈 This tree was auto-generated by [Charcoal](https://github.com/danerwilliams/charcoal) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Chores** * Improved mobile CI workflow with change-aware Android/iOS build jobs and updated completion dependencies so tests wait for the relevant mobile builds. * **Performance / App Behavior** * Enhanced Android WebView behavior: improved viewport/WebView tuning, disabled zoom and scrollbars, and made mixed-content allowance environment-aware (debug vs non-debug). * Adjusted Android cleartext traffic handling based on build/debug settings and Capacitor server URL configuration. * **Tests** * Strengthened Electron BYOK storage tests with per-test temporary directories, mock control, and added coverage for when secure storage is unavailable. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
11
.github/actions/build-rust/action.yml
vendored
11
.github/actions/build-rust/action.yml
vendored
@@ -59,13 +59,20 @@ runs:
|
|||||||
echo "TARGET_CC=clang -D_BSD_SOURCE" >> "$GITHUB_ENV"
|
echo "TARGET_CC=clang -D_BSD_SOURCE" >> "$GITHUB_ENV"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
- name: Prepare cache key
|
||||||
|
id: cache-key
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
shared_key="$(printf '%s' "${{ inputs.target }}-${{ inputs.package }}" | tr -c 'A-Za-z0-9_.-' '-')"
|
||||||
|
echo "shared-key=$shared_key" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Cache cargo
|
- name: Cache cargo
|
||||||
uses: Swatinem/rust-cache@v2
|
uses: Swatinem/rust-cache@v2
|
||||||
if: ${{ runner.os == 'Windows' }}
|
if: ${{ runner.os == 'Windows' }}
|
||||||
with:
|
with:
|
||||||
workspaces: ${{ env.DEV_DRIVE_WORKSPACE }}
|
workspaces: ${{ env.DEV_DRIVE_WORKSPACE }}
|
||||||
save-if: ${{ github.ref_name == 'canary' }}
|
save-if: ${{ github.ref_name == 'canary' }}
|
||||||
shared-key: ${{ inputs.target }}-${{ inputs.package }}
|
shared-key: ${{ steps.cache-key.outputs.shared-key }}
|
||||||
env:
|
env:
|
||||||
CARGO_HOME: ${{ env.DEV_DRIVE }}/.cargo
|
CARGO_HOME: ${{ env.DEV_DRIVE }}/.cargo
|
||||||
RUSTUP_HOME: ${{ env.DEV_DRIVE }}/.rustup
|
RUSTUP_HOME: ${{ env.DEV_DRIVE }}/.rustup
|
||||||
@@ -75,7 +82,7 @@ runs:
|
|||||||
if: ${{ runner.os != 'Windows' }}
|
if: ${{ runner.os != 'Windows' }}
|
||||||
with:
|
with:
|
||||||
save-if: ${{ github.ref_name == 'canary' }}
|
save-if: ${{ github.ref_name == 'canary' }}
|
||||||
shared-key: ${{ inputs.target }}-${{ inputs.package }}
|
shared-key: ${{ steps.cache-key.outputs.shared-key }}
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|||||||
156
.github/workflows/build-test.yml
vendored
156
.github/workflows/build-test.yml
vendored
@@ -135,6 +135,159 @@ jobs:
|
|||||||
echo "All changes are submitted"
|
echo "All changes are submitted"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
mobile-native-build-filter:
|
||||||
|
name: Mobile native build filter
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
run-android: ${{ steps.mobile-native-filter.outputs.android }}
|
||||||
|
run-ios: ${{ steps.mobile-native-filter.outputs.ios }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- uses: dorny/paths-filter@v3
|
||||||
|
id: mobile-native-filter
|
||||||
|
with:
|
||||||
|
filters: |
|
||||||
|
android:
|
||||||
|
- '.github/workflows/build-test.yml'
|
||||||
|
- 'packages/frontend/apps/android/**'
|
||||||
|
- 'packages/frontend/mobile-native/**'
|
||||||
|
- '.cargo/**'
|
||||||
|
- 'Cargo.lock'
|
||||||
|
- 'Cargo.toml'
|
||||||
|
- 'rust-toolchain*'
|
||||||
|
ios:
|
||||||
|
- '.github/workflows/build-test.yml'
|
||||||
|
- 'packages/frontend/apps/ios/**'
|
||||||
|
- 'packages/frontend/mobile-native/**'
|
||||||
|
- '.cargo/**'
|
||||||
|
- 'Cargo.lock'
|
||||||
|
- 'Cargo.toml'
|
||||||
|
- 'rust-toolchain*'
|
||||||
|
|
||||||
|
build-android-app:
|
||||||
|
name: Build Android app
|
||||||
|
if: ${{ needs.mobile-native-build-filter.outputs.run-android == 'true' }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs:
|
||||||
|
- mobile-native-build-filter
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: ./.github/actions/setup-node
|
||||||
|
with:
|
||||||
|
extra-flags: workspaces focus @affine/monorepo @affine-tools/cli @affine/android
|
||||||
|
electron-install: false
|
||||||
|
|
||||||
|
- uses: actions/setup-java@v5
|
||||||
|
with:
|
||||||
|
distribution: 'temurin'
|
||||||
|
java-version: '21'
|
||||||
|
cache: 'gradle'
|
||||||
|
|
||||||
|
- name: Setup Rust
|
||||||
|
uses: ./.github/actions/build-rust
|
||||||
|
with:
|
||||||
|
target: 'aarch64-linux-android'
|
||||||
|
package: 'affine_mobile_native'
|
||||||
|
no-build: 'true'
|
||||||
|
|
||||||
|
- name: Build Android web assets
|
||||||
|
run: yarn affine @affine/android build
|
||||||
|
env:
|
||||||
|
PUBLIC_PATH: '/'
|
||||||
|
|
||||||
|
- name: Write CI Firebase config
|
||||||
|
run: |
|
||||||
|
cat > packages/frontend/apps/android/App/app/google-services.json <<'JSON'
|
||||||
|
{
|
||||||
|
"project_info": {
|
||||||
|
"project_number": "1",
|
||||||
|
"project_id": "affine-ci",
|
||||||
|
"storage_bucket": "affine-ci.appspot.com"
|
||||||
|
},
|
||||||
|
"client": [
|
||||||
|
{
|
||||||
|
"client_info": {
|
||||||
|
"mobilesdk_app_id": "1:1:android:0000000000000000",
|
||||||
|
"android_client_info": {
|
||||||
|
"package_name": "app.affine.pro"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"oauth_client": [],
|
||||||
|
"api_key": [
|
||||||
|
{
|
||||||
|
"current_key": "ci-placeholder"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"services": {
|
||||||
|
"appinvite_service": {
|
||||||
|
"other_platform_oauth_client": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"configuration_version": "1"
|
||||||
|
}
|
||||||
|
JSON
|
||||||
|
|
||||||
|
- name: Cap sync
|
||||||
|
run: yarn workspace @affine/android cap sync
|
||||||
|
|
||||||
|
- name: Build Android debug app
|
||||||
|
working-directory: packages/frontend/apps/android/App
|
||||||
|
run: ./gradlew :app:assembleCanaryDebug --no-daemon --stacktrace
|
||||||
|
|
||||||
|
build-ios-app:
|
||||||
|
name: Build iOS app
|
||||||
|
if: ${{ needs.mobile-native-build-filter.outputs.run-ios == 'true' }}
|
||||||
|
runs-on: macos-15
|
||||||
|
needs:
|
||||||
|
- mobile-native-build-filter
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: ./.github/actions/setup-node
|
||||||
|
with:
|
||||||
|
extra-flags: workspaces focus @affine/monorepo @affine-tools/cli @affine/ios
|
||||||
|
electron-install: false
|
||||||
|
hard-link-nm: false
|
||||||
|
|
||||||
|
- uses: maxim-lobanov/setup-xcode@v1
|
||||||
|
with:
|
||||||
|
xcode-version: 26.2
|
||||||
|
|
||||||
|
- name: Setup Rust
|
||||||
|
uses: ./.github/actions/build-rust
|
||||||
|
with:
|
||||||
|
target: 'aarch64-apple-ios-sim'
|
||||||
|
package: 'affine_mobile_native'
|
||||||
|
no-build: 'true'
|
||||||
|
|
||||||
|
- name: Build iOS web assets
|
||||||
|
run: yarn affine @affine/ios build
|
||||||
|
env:
|
||||||
|
PUBLIC_PATH: '/'
|
||||||
|
|
||||||
|
- name: Cap sync
|
||||||
|
run: yarn workspace @affine/ios sync
|
||||||
|
|
||||||
|
- name: Build iOS simulator app
|
||||||
|
run: |
|
||||||
|
xcodebuild \
|
||||||
|
-workspace packages/frontend/apps/ios/App/App.xcworkspace \
|
||||||
|
-scheme App \
|
||||||
|
-configuration Debug \
|
||||||
|
-sdk iphonesimulator \
|
||||||
|
-destination 'generic/platform=iOS Simulator' \
|
||||||
|
ARCHS=arm64 \
|
||||||
|
ONLY_ACTIVE_ARCH=YES \
|
||||||
|
CODE_SIGNING_ALLOWED=NO \
|
||||||
|
CODE_SIGNING_REQUIRED=NO \
|
||||||
|
build
|
||||||
|
|
||||||
rust-test-filter:
|
rust-test-filter:
|
||||||
name: Rust test filter
|
name: Rust test filter
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -1328,6 +1481,9 @@ jobs:
|
|||||||
- analyze
|
- analyze
|
||||||
- lint
|
- lint
|
||||||
- typecheck
|
- typecheck
|
||||||
|
- mobile-native-build-filter
|
||||||
|
- build-android-app
|
||||||
|
- build-ios-app
|
||||||
- lint-rust
|
- lint-rust
|
||||||
- check-git-status
|
- check-git-status
|
||||||
- check-yarn-binary
|
- check-yarn-binary
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ android {
|
|||||||
versionCode 1
|
versionCode 1
|
||||||
versionName System.getenv('VERSION_NAME') ?: 'v1.0.0-local'
|
versionName System.getenv('VERSION_NAME') ?: 'v1.0.0-local'
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
manifestPlaceholders = [usesCleartextTraffic: "false"]
|
||||||
aaptOptions {
|
aaptOptions {
|
||||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||||
// Default: https://android.googlesource.com/platform/frameworks/base/+/282e181b58cf72b6ca770dc7ca5f91f135444502/tools/aapt/AaptAssets.cpp#61
|
// Default: https://android.googlesource.com/platform/frameworks/base/+/282e181b58cf72b6ca770dc7ca5f91f135444502/tools/aapt/AaptAssets.cpp#61
|
||||||
@@ -49,6 +50,7 @@ android {
|
|||||||
debug {
|
debug {
|
||||||
minifyEnabled false
|
minifyEnabled false
|
||||||
debuggable true
|
debuggable true
|
||||||
|
manifestPlaceholders = [usesCleartextTraffic: "true"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
flavorDimensions = ['chanel']
|
flavorDimensions = ['chanel']
|
||||||
|
|||||||
@@ -13,14 +13,16 @@
|
|||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
|
android:hardwareAccelerated="true"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:usesCleartextTraffic="true"
|
android:usesCleartextTraffic="${usesCleartextTraffic}"
|
||||||
android:theme="@style/AppTheme">
|
android:theme="@style/AppTheme">
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
|
android:hardwareAccelerated="true"
|
||||||
android:label="@string/title_activity_main"
|
android:label="@string/title_activity_main"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
android:theme="@style/AppTheme.NoActionBarLaunch">
|
android:theme="@style/AppTheme.NoActionBarLaunch">
|
||||||
|
|||||||
@@ -93,7 +93,27 @@ class MainActivity : BridgeActivity(), AIButtonPlugin.Callback, AFFiNEThemePlugi
|
|||||||
override fun load() {
|
override fun load() {
|
||||||
super.load()
|
super.load()
|
||||||
AuthInitializer.initialize(bridge)
|
AuthInitializer.initialize(bridge)
|
||||||
bridge.webView.settings.mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
|
configureEditorWebView()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun configureEditorWebView() {
|
||||||
|
bridge.webView.apply {
|
||||||
|
overScrollMode = View.OVER_SCROLL_NEVER
|
||||||
|
isHorizontalScrollBarEnabled = false
|
||||||
|
isVerticalScrollBarEnabled = false
|
||||||
|
settings.apply {
|
||||||
|
// Debug builds may point CAP_SERVER_URL at an HTTP dev server; release builds
|
||||||
|
// should keep mixed content blocked.
|
||||||
|
mixedContentMode = if (BuildConfig.DEBUG) {
|
||||||
|
WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE
|
||||||
|
} else {
|
||||||
|
WebSettings.MIXED_CONTENT_NEVER_ALLOW
|
||||||
|
}
|
||||||
|
setSupportZoom(false)
|
||||||
|
builtInZoomControls = false
|
||||||
|
displayZoomControls = false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun present() {
|
override fun present() {
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ const packageJson = JSON.parse(
|
|||||||
readFileSync(resolve(__dirname, './package.json'), 'utf-8')
|
readFileSync(resolve(__dirname, './package.json'), 'utf-8')
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const capServerUrl = process.env.CAP_SERVER_URL;
|
||||||
|
const allowsCleartextServer = capServerUrl?.startsWith('http://') ?? false;
|
||||||
|
|
||||||
interface AppConfig {
|
interface AppConfig {
|
||||||
affineVersion: string;
|
affineVersion: string;
|
||||||
}
|
}
|
||||||
@@ -27,9 +30,6 @@ const config: CapacitorConfig & AppConfig = {
|
|||||||
},
|
},
|
||||||
adjustMarginsForEdgeToEdge: 'force',
|
adjustMarginsForEdgeToEdge: 'force',
|
||||||
},
|
},
|
||||||
server: {
|
|
||||||
cleartext: true,
|
|
||||||
},
|
|
||||||
plugins: {
|
plugins: {
|
||||||
CapacitorHttp: {
|
CapacitorHttp: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
@@ -40,10 +40,11 @@ const config: CapacitorConfig & AppConfig = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
if (process.env.CAP_SERVER_URL) {
|
if (capServerUrl) {
|
||||||
Object.assign(config, {
|
Object.assign(config, {
|
||||||
server: {
|
server: {
|
||||||
url: process.env.CAP_SERVER_URL,
|
url: capServerUrl,
|
||||||
|
cleartext: allowsCleartextServer,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,3 +2,21 @@ import '@affine/core/bootstrap/browser';
|
|||||||
import '@affine/component/theme';
|
import '@affine/component/theme';
|
||||||
import '@affine/core/mobile/styles/mobile.css';
|
import '@affine/core/mobile/styles/mobile.css';
|
||||||
import './proxy';
|
import './proxy';
|
||||||
|
|
||||||
|
import { viewportRuntimeConfig } from '@blocksuite/affine/std/gfx';
|
||||||
|
|
||||||
|
// Android WebView is less prone to the iOS WKWebView process kill, so keep the
|
||||||
|
// user-facing zoom range unchanged. These knobs only reduce gesture-time work
|
||||||
|
// and low-zoom canvas memory while preserving the normal-zoom render quality.
|
||||||
|
viewportRuntimeConfig.VIEWPORT_REFRESH_PIXEL_THRESHOLD = 60;
|
||||||
|
viewportRuntimeConfig.VIEWPORT_REFRESH_MAX_INTERVAL = 300;
|
||||||
|
viewportRuntimeConfig.SKIP_REFRESH_DURING_GESTURE = true;
|
||||||
|
viewportRuntimeConfig.OVERSCAN_RATIO = 0.2;
|
||||||
|
viewportRuntimeConfig.OVERSCAN_RATIO_BLOCK = 0;
|
||||||
|
// Keep this aligned with iOS: the ~200ms gesture debounce plus this delay gives
|
||||||
|
// elements/connectors enough time to settle while still reappearing within ~500ms.
|
||||||
|
viewportRuntimeConfig.POST_GESTURE_REFRESH_DELAY = 220;
|
||||||
|
viewportRuntimeConfig.CANVAS_DPR_CAP_BY_ZOOM = [
|
||||||
|
[0.5, 1],
|
||||||
|
[0.8, 2],
|
||||||
|
];
|
||||||
|
|||||||
@@ -1,33 +1,62 @@
|
|||||||
|
import os from 'node:os';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
|
|
||||||
import fs from 'fs-extra';
|
import fs from 'fs-extra';
|
||||||
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
|
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
|
||||||
|
|
||||||
const tmpDir = path.join(__dirname, 'tmp-byok-storage');
|
const electronMock = vi.hoisted(() => ({
|
||||||
|
tmpDir: '',
|
||||||
|
appOn: vi.fn(),
|
||||||
|
isEncryptionAvailable: vi.fn(() => true),
|
||||||
|
encryptString: vi.fn((value: string) => Buffer.from(value, 'utf-8')),
|
||||||
|
decryptString: vi.fn((value: Buffer) => value.toString('utf-8')),
|
||||||
|
}));
|
||||||
|
|
||||||
let disposeWorkspaceByokStorage: (() => void) | undefined;
|
let disposeWorkspaceByokStorage: (() => void) | undefined;
|
||||||
|
|
||||||
vi.mock('electron', () => ({
|
vi.mock('electron', () => ({
|
||||||
app: {
|
app: {
|
||||||
getPath: () => tmpDir,
|
getPath: () => electronMock.tmpDir,
|
||||||
on: vi.fn(),
|
on: electronMock.appOn,
|
||||||
},
|
},
|
||||||
safeStorage: {
|
safeStorage: {
|
||||||
isEncryptionAvailable: () => true,
|
isEncryptionAvailable: electronMock.isEncryptionAvailable,
|
||||||
encryptString: (value: string) => Buffer.from(value, 'utf-8'),
|
encryptString: electronMock.encryptString,
|
||||||
decryptString: (value: Buffer) => value.toString('utf-8'),
|
decryptString: electronMock.decryptString,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../../src/main/logger', () => ({
|
||||||
|
logger: {
|
||||||
|
error: vi.fn(),
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
vi.useRealTimers();
|
||||||
vi.resetModules();
|
vi.resetModules();
|
||||||
|
electronMock.appOn.mockReset();
|
||||||
|
electronMock.isEncryptionAvailable.mockReset().mockReturnValue(true);
|
||||||
|
electronMock.encryptString
|
||||||
|
.mockReset()
|
||||||
|
.mockImplementation((value: string) => Buffer.from(value, 'utf-8'));
|
||||||
|
electronMock.decryptString
|
||||||
|
.mockReset()
|
||||||
|
.mockImplementation((value: Buffer) => value.toString('utf-8'));
|
||||||
disposeWorkspaceByokStorage = undefined;
|
disposeWorkspaceByokStorage = undefined;
|
||||||
await fs.remove(tmpDir);
|
electronMock.tmpDir = await fs.mkdtemp(
|
||||||
|
path.join(os.tmpdir(), 'affine-byok-storage-')
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
disposeWorkspaceByokStorage?.();
|
disposeWorkspaceByokStorage?.();
|
||||||
|
disposeWorkspaceByokStorage = undefined;
|
||||||
vi.resetModules();
|
vi.resetModules();
|
||||||
await fs.remove(tmpDir);
|
if (electronMock.tmpDir) {
|
||||||
|
await fs.remove(electronMock.tmpDir);
|
||||||
|
}
|
||||||
|
electronMock.tmpDir = '';
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('byok storage handlers', () => {
|
describe('byok storage handlers', () => {
|
||||||
@@ -84,6 +113,26 @@ describe('byok storage handlers', () => {
|
|||||||
).resolves.toEqual([]);
|
).resolves.toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('does not write local keys when secure storage is unavailable', async () => {
|
||||||
|
electronMock.isEncryptionAvailable.mockReturnValue(false);
|
||||||
|
|
||||||
|
const { byokStorageHandlers, disposeWorkspaceByokStorage: dispose } =
|
||||||
|
await import('@affine/electron/main/byok-storage/handlers');
|
||||||
|
disposeWorkspaceByokStorage = dispose;
|
||||||
|
const ipcEvent = undefined;
|
||||||
|
|
||||||
|
await expect(byokStorageHandlers.isSupported()).resolves.toBe(false);
|
||||||
|
await expect(
|
||||||
|
byokStorageHandlers.upsertWorkspaceKey(ipcEvent, 'workspace-1', {
|
||||||
|
id: 'local-openai',
|
||||||
|
provider: 'openai',
|
||||||
|
name: 'OpenAI',
|
||||||
|
apiKey: 'sk-openai',
|
||||||
|
})
|
||||||
|
).rejects.toThrow('Secure BYOK key storage is not available.');
|
||||||
|
expect(electronMock.encryptString).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
test('preserves existing local key fields during partial updates', async () => {
|
test('preserves existing local key fields during partial updates', async () => {
|
||||||
const { byokStorageHandlers, disposeWorkspaceByokStorage: dispose } =
|
const { byokStorageHandlers, disposeWorkspaceByokStorage: dispose } =
|
||||||
await import('@affine/electron/main/byok-storage/handlers');
|
await import('@affine/electron/main/byok-storage/handlers');
|
||||||
|
|||||||
Reference in New Issue
Block a user