mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-11 03:00:26 +01:00
fix(design-motion): review and QA fixes before ship
- encode: refuse unknown options and --formats tokens, validate --pad (#RRGGBB[AA]) and --crop (compare too) so nothing but a colour or crop reaches ffmpeg's filter graph - encode: export refuses a capture with missing window frames; check prints MOTION_SETTINGS for any threshold/phase override; contact sheet drops -fps_mode - capture: portrait-aware scale, --cycle must be whole 60 fps frames, stills with CSS animations disabled, pauseAt tolerates slow loads, clears stale manifest/stills - skill: <motion-dir> placeholder instead of a cross-block shell variable, GSTACK_FFMPEG-aware probe, stop points before the first capture, doctrine fixes - tests: refusal rows, portrait scale, cycle guard Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
aaa1f989f3
commit
41a720dac4
16 files changed
+173
-64
No files matched your search
@@ -4,7 +4,7 @@ import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { clockMs, planCapture } from '../lib/motion-capture';
|
||||
import {
|
||||
compareArgs, contactSheetArgs, cropError, judgeCheck, loopGifArgs, mp4Args, parsePsnrStats, planChecks, psnrArgs,
|
||||
compareArgs, contactSheetArgs, cropError, judgeCheck, loopGifArgs, mp4Args, padError, parsePsnrStats, planChecks, psnrArgs,
|
||||
resolveFfmpeg, smallGifArgs, FFMPEG_MISSING, type CaptureManifest, type CheckJob, type CheckOptions,
|
||||
} from '../lib/motion-encode';
|
||||
|
||||
@@ -55,6 +55,18 @@ describe('export recipes', () => {
|
||||
expect(cropError('1600x1000')).toContain('w:h:x:y');
|
||||
});
|
||||
|
||||
// Value: protects=values that reach the ffmpeg filter graph (--pad, compare's --crop) are plain colours and w:h:x:y;
|
||||
// fails_when=a value with ',' or ';' reaches the graph and adds filters or inputs; why_new=pad and compare's crop were unvalidated;
|
||||
// seam=none; no_claim=validators only; the CLI refusal rows are in the exit-2 test below
|
||||
test('filter-graph values are validated: a hex pad and a w:h:x:y crop', () => {
|
||||
expect(padError('#f4f1ea')).toBeNull();
|
||||
expect(padError('f4f1eaff')).toBeNull();
|
||||
expect(padError('#fff')).toContain('hex colour');
|
||||
expect(padError('#ffffff,hue=s=0')).toContain('hex colour');
|
||||
expect(cropError('1000:1000:0:0', { ratio: false })).toBeNull();
|
||||
expect(cropError('100:100:0:0,hue=s=0', { ratio: false })).toContain('w:h:x:y');
|
||||
});
|
||||
|
||||
test('the contact sheet tiles twelve frames from the first to the last of the window', () => {
|
||||
const args = contactSheetArgs(m, '/c', '/c/sheet.png');
|
||||
const vf = args[args.indexOf('-vf') + 1]!;
|
||||
@@ -176,4 +188,49 @@ describe('ffmpeg is optional and named when missing', () => {
|
||||
expect(r.exitCode).toBe(2);
|
||||
expect(r.stderr.toString()).toContain('Usage: gstack-motion-encode check');
|
||||
});
|
||||
|
||||
// Value: protects=an unreadable capture, a bad --layout, impossible phases, a missing --name and a non-1.6:1 crop exit 2 before any ffmpeg run;
|
||||
// fails_when=a guard is dropped and ffmpeg runs on bad input or the CLI exits 0/1; why_new=only the no-subcommand usage path was tested; seam=none;
|
||||
// no_claim=a stand-in file satisfies GSTACK_FFMPEG resolution and is never executed, so real ffmpeg is not exercised
|
||||
test('bad captures and bad options exit 2 once ffmpeg resolves, before it runs', () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'motion-encode-usage-'));
|
||||
try {
|
||||
const fake = path.join(dir, 'fake-ffmpeg');
|
||||
fs.writeFileSync(fake, '');
|
||||
const cli = (...args: string[]) => {
|
||||
const r = Bun.spawnSync([BIN, ...args], { stdout: 'pipe', stderr: 'pipe', timeout: 30_000, env: { ...process.env, GSTACK_FFMPEG: fake } });
|
||||
return { code: r.exitCode, err: r.stderr.toString() };
|
||||
};
|
||||
const empty = path.join(dir, 'empty');
|
||||
fs.mkdirSync(empty);
|
||||
const missing = cli('check', empty);
|
||||
expect(missing.code).toBe(2);
|
||||
expect(missing.err).toContain('capture.json');
|
||||
expect(missing.err).toContain('capture first with gstack-motion-capture.ts');
|
||||
const cap = path.join(dir, 'cap');
|
||||
fs.mkdirSync(cap);
|
||||
fs.writeFileSync(path.join(cap, 'capture.json'), JSON.stringify(manifest()));
|
||||
expect(cli('check', cap, '--layout', 'grid')).toEqual({ code: 2, err: '--layout must be app or cards\n' });
|
||||
const phases = cli('check', cap, '--dissolve-at', '13');
|
||||
expect(phases.code).toBe(2);
|
||||
expect(phases.err).toContain('intro-end < dissolve-at <= empty-from < cycle');
|
||||
const name = cli('export', cap, '--name', 'Bad Name');
|
||||
expect(name.code).toBe(2);
|
||||
expect(name.err).toContain('--name <kebab-case-name> is required');
|
||||
const crop = cli('export', cap, '--name', 'ok', '--crop', '1000:1000:0:0');
|
||||
expect(crop.code).toBe(2);
|
||||
expect(crop.err).toContain('is not 1.6:1');
|
||||
// Value: protects=unknown flags, unknown formats, a non-hex pad, a malformed compare crop and missing frames exit 2 before ffmpeg;
|
||||
// fails_when=a typo'd threshold or format is ignored, or export encodes a truncated capture and prints EXPORT_OK;
|
||||
// why_new=these guards are new; seam=none; no_claim=the stand-in ffmpeg is never executed
|
||||
expect(cli('check', cap, '--dissolveat', '10').err).toContain("unknown option '--dissolveat' for check");
|
||||
expect(cli('check', cap, '--dissolveat', '10').code).toBe(2);
|
||||
expect(cli('export', cap, '--name', 'ok', '--formats', 'mp4,gif')).toMatchObject({ code: 2, err: expect.stringContaining("got 'gif'") });
|
||||
expect(cli('export', cap, '--name', 'ok', '--pad', '#ffffff,hue=s=0')).toMatchObject({ code: 2, err: expect.stringContaining('--pad must be a hex colour') });
|
||||
expect(cli('compare', 'a.png', 'b.png', '--crop', '1:1:0:0,hue=s=0')).toMatchObject({ code: 2, err: expect.stringContaining('--crop must be w:h:x:y') });
|
||||
expect(cli('export', cap, '--name', 'ok')).toMatchObject({ code: 2, err: expect.stringContaining('the capture is incomplete') });
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user