diff --git a/studio-app/src/agent-candidate-merge.test.js b/studio-app/src/agent-candidate-merge.test.js index 82be799..007ffef 100644 --- a/studio-app/src/agent-candidate-merge.test.js +++ b/studio-app/src/agent-candidate-merge.test.js @@ -66,3 +66,47 @@ test('foreign project results cannot attach to the quick project even when artif assert.equal(mergeJobCandidate(project, job).changed, false); assert.deepEqual(project, before); }); + +function freshResult(changes = {}) { + const project = freestyleProject('New takes'), + shot = project.entities.find((e) => e.type === 'shot'); + shot.data.selectedAssetId = 'older-take'; + const job = { + id: 'job-new', + status: 'succeeded', + recipe_id: 'h3-base-fl2va-v1', + client_ref: { project_id: project.id, shot_id: shot.id, shot_version: shot.version }, + artifacts: [{ id: 'art-new', kind: 'video', content_type: 'video/mp4' }], + ...changes, + }; + return { project, shot, job }; +} +test('a new take becomes the current take of its shot', () => { + const { project, shot, job } = freshResult(); + mergeJobCandidate(project, job); + assert.equal(shot.data.selectedAssetId, 'result-art-new'); +}); +test('a shot cut into segments keeps its current take', () => { + const { project, shot, job } = freshResult(); + shot.data.videoSegments = [{ start: 0, end: 2 }]; + mergeJobCandidate(project, job); + assert.equal(shot.data.selectedAssetId, 'older-take'); + assert.ok(shot.data.candidateIds.includes('result-art-new')); +}); +test('a take from an older shot version waits; an enhanced copy does not', () => { + const stale = freshResult(); + stale.job.client_ref.shot_version = stale.shot.version - 1; + mergeJobCandidate(stale.project, stale.job); + assert.equal(stale.shot.data.selectedAssetId, 'older-take'); + const enhanced = freshResult({ recipe_id: 'enhance-v1' }); + enhanced.job.client_ref.shot_version = enhanced.shot.version - 1; + mergeJobCandidate(enhanced.project, enhanced.job); + assert.equal(enhanced.shot.data.selectedAssetId, 'result-art-new'); +}); +test('polling the same finished job again does not undo a later choice', () => { + const { project, shot, job } = freshResult(); + mergeJobCandidate(project, job); + shot.data.selectedAssetId = 'older-take'; + mergeJobCandidate(project, job); + assert.equal(shot.data.selectedAssetId, 'older-take'); +}); diff --git a/studio-app/src/cloud-model.js b/studio-app/src/cloud-model.js index f7d1af6..2386122 100644 --- a/studio-app/src/cloud-model.js +++ b/studio-app/src/cloud-model.js @@ -1,3 +1,4 @@ +import { ENHANCE_RECIPE } from './enhance-take.js'; import { formatMoneyMicros } from './money.js'; import { translate } from './i18n/locale.js'; import { boundVideoRange } from './video-cut-model.js'; @@ -871,6 +872,15 @@ function importArtifact(project, job, artifact, { shot, old, same, audioSeen, no if (shot && !isAudio) { shot.data.candidateIds = [...new Set([...(shot.data.candidateIds || []), id])]; if (old) shot.status = 'review'; + // A new take becomes the shot's take (owner, 2026-11-02). Two cases stay the person's call: + // a take from an older version of the shot (an enhanced copy never is, its source is the + // same take), and a shot already cut into segments, which belong to the current file. + if ( + artifact.kind === 'video' && + (!old || job.recipe_id === ENHANCE_RECIPE) && + !(shot.data.videoSegments || []).length + ) + shot.data.selectedAssetId = id; } return id; } diff --git a/studio-app/src/storyboard-cloud.test.js b/studio-app/src/storyboard-cloud.test.js index 064b355..e9c4c17 100644 --- a/studio-app/src/storyboard-cloud.test.js +++ b/studio-app/src/storyboard-cloud.test.js @@ -464,7 +464,7 @@ test('successful saved storyboard job uses shot version without mistaking the se assert.equal(result.data.oldVersion, false); assert.equal( !!h.store.exportProject().entities.find((e) => e.id === h.shot).data.selectedAssetId, - false, + true, ); h.store.editProject( (p) => diff --git a/studio-app/src/storyboard-domain.test.js b/studio-app/src/storyboard-domain.test.js index b4ff4e9..e2d4942 100644 --- a/studio-app/src/storyboard-domain.test.js +++ b/studio-app/src/storyboard-domain.test.js @@ -579,7 +579,7 @@ test('profile look imageIds empty explicitly clears gallery while absent imageId assert.deepEqual(looks[0].images, []); assert.deepEqual(looks[1].images, ['photo']); }); -test('look voice binding and chosen asset remain explicit; succeeded new jobs do not auto-adopt', () => { +test('look voice binding stays explicit; a succeeded new job becomes the chosen take', () => { const p = fixture(); const hero = character('hero', 'photo', 'main-voice'); hero.data.voices.push({ id: 'other', assetId: 'look-voice', clipStart: 3 }); @@ -596,7 +596,7 @@ test('look voice binding and chosen asset remain explicit; succeeded new jobs do client_ref: { project_id: 'project', shot_id: 'shot', shot_version: 1 }, artifacts: [{ id: 'new-result', kind: 'video', mime: 'video/mp4' }], }); - assert.equal(view(p).shots[0].chosen, 'keep'); + assert.equal(view(p).shots[0].chosen, 'result-new-result'); assert.ok(view(p).shots[0].versions.some((v) => v.artifactId === 'new-result')); }); test('storyboard preflight snapshot invalidates when shared look, clip start or global style changes', async () => {