Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions studio-app/src/agent-candidate-merge.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
10 changes: 10 additions & 0 deletions studio-app/src/cloud-model.js
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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;
}
Expand Down
2 changes: 1 addition & 1 deletion studio-app/src/storyboard-cloud.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) =>
Expand Down
4 changes: 2 additions & 2 deletions studio-app/src/storyboard-domain.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand All @@ -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 () => {
Expand Down
Loading