mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-03 18:40:00 +08:00
feat(native): record encoding (#14188)
fix #13784 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Start/stop system or meeting recordings with Ogg/Opus artifacts and native start/stop APIs; workspace backup recovery. * **Refactor** * Simplified recording lifecycle and UI flows; native runtime now orchestrates recording/processing and reporting. * **Bug Fixes** * Stronger path validation, safer import/export dialogs, consistent error handling/logging, and retry-safe recording processing. * **Chores** * Added cross-platform native audio capture and Ogg/Opus encoding support. * **Tests** * New unit, integration, and e2e tests for recording, path guards, dialogs, and workspace recovery. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -21,6 +21,11 @@ const docSetSpaceId = vi.fn();
|
||||
const sqliteValidate = vi.fn();
|
||||
const sqliteValidateImportSchema = vi.fn();
|
||||
const sqliteVacuumInto = vi.fn();
|
||||
const sqliteClose = vi.fn();
|
||||
const showOpenDialog = vi.fn();
|
||||
const showSaveDialog = vi.fn();
|
||||
const showItemInFolder = vi.fn(async () => undefined);
|
||||
const getPath = vi.fn();
|
||||
|
||||
vi.doMock('nanoid', () => ({
|
||||
nanoid: () => 'workspace-1',
|
||||
@@ -70,6 +75,10 @@ vi.doMock('@affine/native', () => {
|
||||
vacuumInto(path: string) {
|
||||
return sqliteVacuumInto(this.path, path);
|
||||
}
|
||||
|
||||
close() {
|
||||
return sqliteClose(this.path);
|
||||
}
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -84,7 +93,10 @@ vi.doMock('@affine/electron/helper/nbstore', () => ({
|
||||
|
||||
vi.doMock('@affine/electron/helper/main-rpc', () => ({
|
||||
mainRPC: {
|
||||
showItemInFolder: vi.fn(),
|
||||
getPath,
|
||||
showItemInFolder,
|
||||
showOpenDialog,
|
||||
showSaveDialog,
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -126,12 +138,11 @@ describe('dialog export', () => {
|
||||
realpath.mockImplementation(async path => path);
|
||||
getSpaceDBPath.mockResolvedValue(dbPath);
|
||||
move.mockResolvedValue(undefined);
|
||||
showSaveDialog.mockResolvedValue({ canceled: false, filePath: exportPath });
|
||||
|
||||
const { saveDBFileAs, setFakeDialogResult } =
|
||||
const { saveDBFileAs } =
|
||||
await import('@affine/electron/helper/dialog/dialog');
|
||||
|
||||
setFakeDialogResult({ filePath: exportPath });
|
||||
|
||||
const result = await saveDBFileAs(id, 'My Space');
|
||||
|
||||
expect(result).toEqual({ filePath: exportPath });
|
||||
@@ -151,12 +162,11 @@ describe('dialog export', () => {
|
||||
|
||||
pathExists.mockResolvedValue(false);
|
||||
getSpaceDBPath.mockResolvedValue(dbPath);
|
||||
showSaveDialog.mockResolvedValue({ canceled: false, filePath: dbPath });
|
||||
|
||||
const { saveDBFileAs, setFakeDialogResult } =
|
||||
const { saveDBFileAs } =
|
||||
await import('@affine/electron/helper/dialog/dialog');
|
||||
|
||||
setFakeDialogResult({ filePath: dbPath });
|
||||
|
||||
const result = await saveDBFileAs(id, 'My Space');
|
||||
|
||||
expect(result).toEqual({ error: 'DB_FILE_PATH_INVALID' });
|
||||
@@ -174,12 +184,11 @@ describe('dialog export', () => {
|
||||
path === exportPath ? dbPath : path
|
||||
);
|
||||
getSpaceDBPath.mockResolvedValue(dbPath);
|
||||
showSaveDialog.mockResolvedValue({ canceled: false, filePath: exportPath });
|
||||
|
||||
const { saveDBFileAs, setFakeDialogResult } =
|
||||
const { saveDBFileAs } =
|
||||
await import('@affine/electron/helper/dialog/dialog');
|
||||
|
||||
setFakeDialogResult({ filePath: exportPath });
|
||||
|
||||
const result = await saveDBFileAs(id, 'My Space');
|
||||
|
||||
expect(result).toEqual({ error: 'DB_FILE_PATH_INVALID' });
|
||||
@@ -193,6 +202,12 @@ describe('dialog import', () => {
|
||||
const originalPath = '/tmp/import.affine';
|
||||
const internalPath = '/app/workspaces/local/workspace-1/storage.db';
|
||||
|
||||
pathExists.mockResolvedValue(true);
|
||||
realpath.mockImplementation(async path => path);
|
||||
showOpenDialog.mockResolvedValue({
|
||||
canceled: false,
|
||||
filePaths: [originalPath],
|
||||
});
|
||||
getWorkspacesBasePath.mockResolvedValue('/app/workspaces');
|
||||
getSpaceDBPath.mockResolvedValue(internalPath);
|
||||
docValidate.mockResolvedValue(true);
|
||||
@@ -201,11 +216,9 @@ describe('dialog import', () => {
|
||||
docSetSpaceId.mockResolvedValue(undefined);
|
||||
ensureDir.mockResolvedValue(undefined);
|
||||
|
||||
const { loadDBFile, setFakeDialogResult } =
|
||||
const { loadDBFile } =
|
||||
await import('@affine/electron/helper/dialog/dialog');
|
||||
|
||||
setFakeDialogResult({ filePath: originalPath });
|
||||
|
||||
const result = await loadDBFile();
|
||||
|
||||
expect(result).toEqual({ workspaceId: 'workspace-1' });
|
||||
@@ -219,15 +232,19 @@ describe('dialog import', () => {
|
||||
test('loadDBFile rejects v2 imports with unexpected schema objects', async () => {
|
||||
const originalPath = '/tmp/import.affine';
|
||||
|
||||
pathExists.mockResolvedValue(true);
|
||||
realpath.mockImplementation(async path => path);
|
||||
showOpenDialog.mockResolvedValue({
|
||||
canceled: false,
|
||||
filePaths: [originalPath],
|
||||
});
|
||||
getWorkspacesBasePath.mockResolvedValue('/app/workspaces');
|
||||
docValidate.mockResolvedValue(true);
|
||||
docValidateImportSchema.mockResolvedValue(false);
|
||||
|
||||
const { loadDBFile, setFakeDialogResult } =
|
||||
const { loadDBFile } =
|
||||
await import('@affine/electron/helper/dialog/dialog');
|
||||
|
||||
setFakeDialogResult({ filePath: originalPath });
|
||||
|
||||
const result = await loadDBFile();
|
||||
|
||||
expect(result).toEqual({ error: 'DB_FILE_INVALID' });
|
||||
@@ -239,6 +256,12 @@ describe('dialog import', () => {
|
||||
const originalPath = '/tmp/import-v1.affine';
|
||||
const internalPath = '/app/workspaces/workspace-1/storage.db';
|
||||
|
||||
pathExists.mockResolvedValue(true);
|
||||
realpath.mockImplementation(async path => path);
|
||||
showOpenDialog.mockResolvedValue({
|
||||
canceled: false,
|
||||
filePaths: [originalPath],
|
||||
});
|
||||
getWorkspacesBasePath.mockResolvedValue('/app/workspaces');
|
||||
getWorkspaceDBPath.mockResolvedValue(internalPath);
|
||||
docValidate.mockResolvedValue(false);
|
||||
@@ -247,11 +270,9 @@ describe('dialog import', () => {
|
||||
sqliteVacuumInto.mockResolvedValue(undefined);
|
||||
ensureDir.mockResolvedValue(undefined);
|
||||
|
||||
const { loadDBFile, setFakeDialogResult } =
|
||||
const { loadDBFile } =
|
||||
await import('@affine/electron/helper/dialog/dialog');
|
||||
|
||||
setFakeDialogResult({ filePath: originalPath });
|
||||
|
||||
const result = await loadDBFile();
|
||||
|
||||
expect(result).toEqual({ workspaceId: 'workspace-1' });
|
||||
@@ -263,6 +284,57 @@ describe('dialog import', () => {
|
||||
id: 'workspace-1',
|
||||
mainDBPath: internalPath,
|
||||
});
|
||||
expect(sqliteClose).toHaveBeenCalledWith(originalPath);
|
||||
expect(copy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('loadDBFile closes v1 connection when schema validation fails', async () => {
|
||||
const originalPath = '/tmp/import-v1-invalid.affine';
|
||||
|
||||
pathExists.mockResolvedValue(true);
|
||||
realpath.mockImplementation(async path => path);
|
||||
showOpenDialog.mockResolvedValue({
|
||||
canceled: false,
|
||||
filePaths: [originalPath],
|
||||
});
|
||||
getWorkspacesBasePath.mockResolvedValue('/app/workspaces');
|
||||
docValidate.mockResolvedValue(false);
|
||||
sqliteValidate.mockResolvedValue('Valid');
|
||||
sqliteValidateImportSchema.mockResolvedValue(false);
|
||||
|
||||
const { loadDBFile } =
|
||||
await import('@affine/electron/helper/dialog/dialog');
|
||||
|
||||
const result = await loadDBFile();
|
||||
|
||||
expect(result).toEqual({ error: 'DB_FILE_INVALID' });
|
||||
expect(sqliteClose).toHaveBeenCalledWith(originalPath);
|
||||
expect(sqliteVacuumInto).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('loadDBFile rejects normalized paths inside app data', async () => {
|
||||
const selectedPath = '/tmp/import.affine';
|
||||
const normalizedPath = '/app/workspaces/local/existing/storage.db';
|
||||
|
||||
pathExists.mockResolvedValue(true);
|
||||
realpath.mockImplementation(async path => {
|
||||
if (path === selectedPath) {
|
||||
return normalizedPath;
|
||||
}
|
||||
return path;
|
||||
});
|
||||
showOpenDialog.mockResolvedValue({
|
||||
canceled: false,
|
||||
filePaths: [selectedPath],
|
||||
});
|
||||
getWorkspacesBasePath.mockResolvedValue('/app/workspaces');
|
||||
|
||||
const { loadDBFile } =
|
||||
await import('@affine/electron/helper/dialog/dialog');
|
||||
|
||||
const result = await loadDBFile();
|
||||
|
||||
expect(result).toEqual({ error: 'DB_FILE_PATH_INVALID' });
|
||||
expect(docValidate).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user