From 539e598eacc7926747b32a531ba7ee720def132d Mon Sep 17 00:00:00 2001 From: EtienneLescot Date: Mon, 2 Nov 2026 21:13:55 +0100 Subject: [PATCH 1/3] fix(cli): keep legacy editor keys when exporting a v2 project normalizeProjectEditor keeps an allowlist, so loadLegacyProject dropped every key getEditorSettings reads beyond it: the 3D cursor, click impact and sound, webcam styling and audio gain. A CLI export rendered them at their defaults. Pass the raw editor through; the normalized values still win and every dropped key is validated where it is read. Fixes #1467 --- src/cli/loadCliProject.test.ts | 38 ++++++++++++++++++++++++++++++++++ src/cli/loadCliProject.ts | 16 +++++++++++++- 2 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 src/cli/loadCliProject.test.ts diff --git a/src/cli/loadCliProject.test.ts b/src/cli/loadCliProject.test.ts new file mode 100644 index 0000000000..b04ac1a88b --- /dev/null +++ b/src/cli/loadCliProject.test.ts @@ -0,0 +1,38 @@ +import { describe, expect, it, vi } from "vitest"; +import { PROJECT_VERSION } from "@/components/video-editor/projectPersistence"; +import { migrateProjectDataToAxcutDocument } from "@/lib/ai-edition/document/migrate"; +import { getEditorSettings } from "@/lib/ai-edition/store/editorSettings"; +import { legacyEditorForMigration } from "./loadCliProject"; + +vi.mock("@/native", () => ({ nativeBridgeClient: {} })); + +const migrate = (editor: Record) => + migrateProjectDataToAxcutDocument({ + version: PROJECT_VERSION, + media: { screenVideoPath: "/recordings/screen.mp4" }, + editor: legacyEditorForMigration(editor), + }); + +describe("legacyEditorForMigration", () => { + it("keeps the editor keys normalizeProjectEditor does not list (#1467)", () => { + const settings = getEditorSettings( + migrate({ + cursorModel3d: true, + cursorClickImpact: true, + cursorClickSound: true, + cursorClickSoundGainDb: -4, + audioGainDb: -3, + }), + ); + expect(settings.cursor.model3d).toBe(true); + expect(settings.cursor.clickImpact).toBe(true); + expect(settings.cursor.clickSound).toBe(true); + expect(settings.cursor.clickSoundGainDb).toBe(-4); + expect(settings.audioGainDb).toBe(-3); + }); + + it("lets the normalized value win over the raw one", () => { + const settings = getEditorSettings(migrate({ aspectRatio: "not-a-ratio" })); + expect(settings.aspectRatio).not.toBe("not-a-ratio"); + }); +}); diff --git a/src/cli/loadCliProject.ts b/src/cli/loadCliProject.ts index 395514348b..774d721cdb 100644 --- a/src/cli/loadCliProject.ts +++ b/src/cli/loadCliProject.ts @@ -5,6 +5,7 @@ import { type EditorProjectData, normalizeProjectEditor, + type ProjectEditorState, resolveProjectMedia, toFileUrl, validateProjectData, @@ -47,6 +48,19 @@ export function probeVideoDimensions( }); } +/** + * The editor a v2 project migrates with: normalized, plus every key normalization does not list. + * `normalizeProjectEditor` keeps an allowlist, while `getEditorSettings` reads more of + * `legacyEditor` than that (the 3D cursor, click impact and sound, webcam styling, audio gain), + * each key with its own validation and default. Dropping them made a CLI export render those + * settings at their defaults (#1467); passed through raw, they are still read safely. + */ +export function legacyEditorForMigration( + editor: Partial = {}, +): ProjectEditorState { + return { ...editor, ...normalizeProjectEditor(editor) }; +} + /** * A legacy v2 project (`{ version, media, editor }`), as written before the editor saved the * document itself: migrated to an AxcutDocument, with what the migration cannot know probed. @@ -70,7 +84,7 @@ async function loadLegacyProject(project: EditorProjectData): Promise Date: Mon, 2 Nov 2026 21:22:00 +0100 Subject: [PATCH 2/3] test(cli): cover legacy editor keys through the v2 loader --- src/cli/loadCliProject.test.ts | 50 ++++++++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/src/cli/loadCliProject.test.ts b/src/cli/loadCliProject.test.ts index b04ac1a88b..6de0c2db3d 100644 --- a/src/cli/loadCliProject.test.ts +++ b/src/cli/loadCliProject.test.ts @@ -2,9 +2,14 @@ import { describe, expect, it, vi } from "vitest"; import { PROJECT_VERSION } from "@/components/video-editor/projectPersistence"; import { migrateProjectDataToAxcutDocument } from "@/lib/ai-edition/document/migrate"; import { getEditorSettings } from "@/lib/ai-edition/store/editorSettings"; -import { legacyEditorForMigration } from "./loadCliProject"; +import { legacyEditorForMigration, loadCliProject } from "./loadCliProject"; -vi.mock("@/native", () => ({ nativeBridgeClient: {} })); +const loadProjectFileFromPath = vi.fn(); +vi.mock("@/native", () => ({ + nativeBridgeClient: { + project: { loadProjectFileFromPath: (p: string) => loadProjectFileFromPath(p) }, + }, +})); const migrate = (editor: Record) => migrateProjectDataToAxcutDocument({ @@ -36,3 +41,44 @@ describe("legacyEditorForMigration", () => { expect(settings.aspectRatio).not.toBe("not-a-ratio"); }); }); + +describe("loadCliProject", () => { + it("carries those keys through the v2 loader into the exported document (#1467)", async () => { + loadProjectFileFromPath.mockResolvedValue({ + success: true, + project: { + version: PROJECT_VERSION, + media: { screenVideoPath: "/recordings/screen.mp4" }, + editor: { cursorModel3d: true, cursorClickSound: true }, + }, + }); + vi.stubGlobal("window", { + electronAPI: { getCurrentRecordingSession: async () => ({ session: null }) }, + }); + // The probe's