fix(cli): preserve authored clip length when moving (#5315)

Signed-off-by: Rudy Celekli <rudy@gradiahq.com>
This commit is contained in:
Rudy Mizrahi Celekli
2026-10-24 08:15:49 +00:00
committed by GitHub
parent 6c5ad56e46
commit 004409e570
2 changed files with 89 additions and 2 deletions
+9 -2
View File
@@ -191,9 +191,16 @@ function moveMutation(context: MutationContext, args: Record<string, unknown>):
fix: `choose a start at or before the latest valid start ${context.duration - context.row.duration}`,
};
}
const patched = patchElementInHtml(context.before, context.resolved.target, [
const operations: PatchOperation[] = [
{ type: "html-attribute", property: "data-start", value: String(time.seconds) },
]);
];
if (context.row.durationAuthored) {
operations.push(
{ type: "html-attribute", property: "data-duration", value: String(context.row.duration) },
{ type: "html-attribute", property: "data-end", value: null },
);
}
const patched = patchElementInHtml(context.before, context.resolved.target, operations);
if (!patched.matched) {
return { ok: false, reason: `${context.ref} was not found`, fix: "choose an existing clip" };
}
@@ -871,3 +871,83 @@ describe("timeline edit command", () => {
}
});
});
describe("timeline move preserves clip length", () => {
const cases = [
{
name: "end-authored clip moving forward",
clip: '<div id="clip" data-hf-id="clip" data-start="1" data-end="3" data-track-index="0"></div>',
start: 2,
duration: 2,
},
{
name: "end-authored clip moving backward",
clip: '<div id="clip" data-hf-id="clip" data-start="4" data-end="6" data-track-index="0"></div>',
start: 1,
duration: 2,
},
{
name: "duration-authored clip",
clip: '<div id="clip" data-hf-id="clip" data-start="1" data-duration="2" data-track-index="0"></div>',
start: 2,
duration: 2,
},
{
name: "image with its default duration",
clip: '<img id="clip" data-hf-id="clip" data-start="1" data-track-index="0">',
start: 2,
duration: 3,
},
];
for (const planned of [false, true]) {
it.each(cases)(
`moves $name without changing its length (plan=${planned})`,
({ clip, start, duration }) => {
const dir = mkdtempSync(join(tmpdir(), "hf-timeline-move-length-"));
const indexPath = join(dir, "index.html");
const source = `<div data-composition-id="main" data-duration="12">${clip}</div>`;
writeFileSync(indexPath, source);
try {
const result = run(dir, "move", "#clip", String(start), ...(planned ? ["--plan"] : []));
expect(result.status, result.stderr).toBe(0);
const output: unknown = JSON.parse(result.stdout);
expect(output).toMatchObject({
planned,
before: expect.arrayContaining([expect.objectContaining({ ref: "#clip", duration })]),
after: expect.arrayContaining([
expect.objectContaining({ ref: "#clip", start, duration, end: start + duration }),
]),
});
if (planned) expect(readFileSync(indexPath, "utf8")).toBe(source);
else {
const inspected = spawnSync("bun", ["run", cliEntry, "timeline", dir, "--json"], {
cwd: dir,
encoding: "utf8",
timeout: 30_000,
env: { ...process.env, HYPERFRAMES_SKIP_UPDATE_CHECK: "1" },
});
expect(inspected.status, inspected.stderr).toBe(0);
expect(JSON.parse(inspected.stdout)).toMatchObject({
timeline: {
tracks: expect.arrayContaining([
expect.objectContaining({
rows: expect.arrayContaining([
expect.objectContaining({
ref: "#clip",
start,
duration,
end: start + duration,
}),
]),
}),
]),
},
});
}
} finally {
rmSync(dir, { recursive: true, force: true });
}
},
);
}
});