Skip to content

Commit 3e4828d

Browse files
szdziedzicthymikee
andauthored
feat: add scale-only screenshot sizing (#1617)
* feat: add scale-only screenshot sizing * fix: refuse retired --max-size inputs on every released surface Released sizing inputs must fail closed with migration guidance instead of silently producing native-size artifacts: - contracts: RETIRED_SCREENSHOT_MAX_SIZE declaration + SCREENSHOT_SCALE_LIMITS as the single source for the scale bounds and migration messages - .ad parser: released 'screenshot ... --max-size N' and 'record start ... --max-size N' lines now refuse at parse time (frozen replay-compat witnesses) - daemon: screenshot rejects old-client screenshotMaxSize like recording does; the recording guard now shares the same contract data - Node client: screenshot/record daemon writers refuse the removed { maxSize } option before transport - CLI: --max-size unknown-flag error carries the migration guidance - config/env: stale screenshotMaxSize config keys and the retired AGENT_DEVICE_SCREENSHOT_MAX_SIZE env var are refused for sizing commands (other commands keep working) Quality: numberField now reuses the canonical readOptionalNumber contract helper (AppError bounds instead of plain Error); png-resize inlines one-use wrappers and restores the worker-thread rationale; docs typo fixed. * test: drop retired maxSize entries from the MCP undocumented-input allowlist * fix: refuse retired maxSize at the MCP field-projection seam + release-provenance corpus witnesses - readFieldInput silently dropped undeclared keys before the daemon writers could refuse them, so an MCP call carrying { maxSize } reached transport and returned native-size success. New retiredField() combinator declares the removed key in the field map: the projection seam refuses it with the canonical migration message and the JSON schema no longer advertises it. Real-route MCP executor regressions cover screenshot and record. - replay-compat corpus: derived v0.20.5 witnesses for the released screenshot and record --max-size forms (SHA-256 pinned, new retired-capture-size coverage surface) so check:replay-compat proves the shipped syntax refuses with migration guidance instead of degrading silently. --------- Co-authored-by: Michał Pierzchała <thymikee@gmail.com>
1 parent f93b259 commit 3e4828d

82 files changed

Lines changed: 796 additions & 822 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1506,24 +1506,18 @@ extension RunnerTests {
15061506
if let requestedFps = command.fps, (requestedFps < minRecordingFps || requestedFps > maxRecordingFps) {
15071507
return Response(ok: false, error: ErrorPayload(message: "recordStart fps must be between \(minRecordingFps) and \(maxRecordingFps)"))
15081508
}
1509-
if let requestedMaxSize = command.maxSize, requestedMaxSize < 1 {
1510-
return Response(ok: false, error: ErrorPayload(message: "recordStart maxSize must be a positive integer"))
1511-
}
15121509
do {
15131510
let resolvedOutPath = resolveRecordingOutPath(requestedOutPath)
15141511
let fpsLabel = command.fps.map(String.init) ?? String(RunnerTests.defaultRecordingFps)
1515-
let maxSizeLabel = command.maxSize.map(String.init) ?? "native"
15161512
NSLog(
1517-
"AGENT_DEVICE_RUNNER_RECORD_START requestedOutPath=%@ resolvedOutPath=%@ fps=%@ maxSize=%@",
1513+
"AGENT_DEVICE_RUNNER_RECORD_START requestedOutPath=%@ resolvedOutPath=%@ fps=%@",
15181514
requestedOutPath,
15191515
resolvedOutPath,
1520-
fpsLabel,
1521-
maxSizeLabel
1516+
fpsLabel
15221517
)
15231518
let recorder = ScreenRecorder(
15241519
outputPath: resolvedOutPath,
1525-
fps: command.fps.map { Int32($0) },
1526-
maxSize: command.maxSize
1520+
fps: command.fps.map { Int32($0) }
15271521
)
15281522
try recorder.start { [weak self] in
15291523
return self?.captureRunnerFrame()

apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,6 @@ struct Command: Codable {
136136
let gesturePlan: RunnerGesturePlan?
137137
let outPath: String?
138138
let fps: Int?
139-
let maxSize: Int?
140139
let interactiveOnly: Bool?
141140
let preferredBackend: String?
142141
let depth: Int?

apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+ScreenRecorder.swift

Lines changed: 2 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ extension RunnerTests {
77
final class ScreenRecorder {
88
private let outputPath: String
99
private let fps: Int32?
10-
private let maxSize: Int?
1110
private var effectiveFps: Int32 {
1211
max(1, fps ?? RunnerTests.defaultRecordingFps)
1312
}
@@ -26,10 +25,9 @@ extension RunnerTests {
2625
private var startedSession = false
2726
private var startError: Error?
2827

29-
init(outputPath: String, fps: Int32?, maxSize: Int?) {
28+
init(outputPath: String, fps: Int32?) {
3029
self.outputPath = outputPath
3130
self.fps = fps
32-
self.maxSize = maxSize
3331
}
3432

3533
func start(captureFrame: @escaping () -> RunnerImage?) throws {
@@ -50,7 +48,7 @@ extension RunnerTests {
5048
while Date() < bootstrapDeadline {
5149
if let image = captureFrame(), let cgImage = runnerCGImage(from: image) {
5250
bootstrapImage = image
53-
dimensions = scaledDimensions(width: cgImage.width, height: cgImage.height)
51+
dimensions = CGSize(width: cgImage.width, height: cgImage.height)
5452
break
5553
}
5654
Thread.sleep(forTimeInterval: 0.05)
@@ -261,23 +259,5 @@ extension RunnerTests {
261259
return pixelBuffer
262260
}
263261

264-
private func scaledDimensions(width: Int, height: Int) -> CGSize {
265-
guard let maxSize, maxSize > 0 else {
266-
return CGSize(width: width, height: height)
267-
}
268-
let longest = max(width, height)
269-
guard longest > maxSize else {
270-
return CGSize(width: width, height: height)
271-
}
272-
let scale = Double(maxSize) / Double(longest)
273-
return CGSize(
274-
width: scaledEvenDimension(width, scale: scale),
275-
height: scaledEvenDimension(height, scale: scale)
276-
)
277-
}
278-
279-
private func scaledEvenDimension(_ value: Int, scale: Double) -> Int {
280-
max(2, Int((Double(value) * scale / 2.0).rounded()) * 2)
281-
}
282262
}
283263
}

apple/runner/AgentDeviceRunner/RecordingScripts/recording-resize.swift

Lines changed: 0 additions & 225 deletions
This file was deleted.

apple/runner/RUNNER_PROTOCOL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ Examples:
3232
```
3333

3434
```json
35-
{ "command": "recordStart", "outPath": "/tmp/demo.mp4", "fps": 30, "maxSize": 720 }
35+
{ "command": "recordStart", "outPath": "/tmp/demo.mp4", "fps": 30 }
3636
```
3737

3838
```json

packages/ad-script/src/internal/__tests__/script.test.ts

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -50,18 +50,35 @@ test('formatPortableActionLine preserves inline open runtime hints', () => {
5050
);
5151
});
5252

53-
test('record replay script parses fps, max-size, quality, and hide-touches flags', () => {
54-
const script =
55-
'record start "./capture.mp4" --fps 24 --max-size 1024 --quality high --hide-touches\n';
53+
test('record replay script parses fps, quality, and hide-touches flags', () => {
54+
const script = 'record start "./capture.mp4" --fps 24 --quality high --hide-touches\n';
5655
const parsed = parseReplayScriptDetailed(script).actions;
5756

5857
assert.deepEqual(parsed[0]?.positionals, ['start', './capture.mp4']);
5958
assert.equal(parsed[0]?.flags.fps, 24);
60-
assert.equal(parsed[0]?.flags.screenshotMaxSize, 1024);
6159
assert.equal(parsed[0]?.flags.quality, 'high');
6260
assert.equal(parsed[0]?.flags.hideTouches, true);
6361
});
6462

63+
// Parser-level witnesses of the retired `--max-size` refusal. The
64+
// release-provenance frozen forms live in the replay-compat corpus
65+
// (test/replay-compat/scripts/docs/{screenshot,record}-max-size.v0.20.5.ad);
66+
// these fast unit copies pin the same behavior at the parse seam: refusal
67+
// with migration guidance, never a silent degrade into extra positionals.
68+
test('released screenshot --max-size lines are refused with migration guidance', () => {
69+
assert.throws(() => parseReplayScriptDetailed('screenshot "./page.png" --max-size 1024\n'), {
70+
code: 'INVALID_ARGS',
71+
message: /screenshot --max-size was removed; use --scale/,
72+
});
73+
});
74+
75+
test('released record --max-size lines are refused with migration guidance', () => {
76+
assert.throws(() => parseReplayScriptDetailed('record start "./capture.mp4" --max-size 1024\n'), {
77+
code: 'INVALID_ARGS',
78+
message: /record --max-size was removed/,
79+
});
80+
});
81+
6582
test('screenshot replay script round-trips screenshot flags', () => {
6683
const actions: SessionAction[] = [
6784
{
@@ -71,7 +88,7 @@ test('screenshot replay script round-trips screenshot flags', () => {
7188
flags: {
7289
screenshotPixelDensity: 2,
7390
screenshotFullscreen: true,
74-
screenshotMaxSize: 1024,
91+
screenshotScale: 0.3,
7592
screenshotNoStabilize: true,
7693
},
7794
},
@@ -80,14 +97,14 @@ test('screenshot replay script round-trips screenshot flags', () => {
8097
const script = formatReplayScriptForTest(actions);
8198
assert.match(
8299
script,
83-
/screenshot "\.\/page\.png" --pixel-density 2 --fullscreen --max-size 1024 --no-stabilize/,
100+
/screenshot "\.\/page\.png" --pixel-density 2 --fullscreen --scale 0.3 --no-stabilize/,
84101
);
85102

86103
const parsed = parseReplayScriptDetailed(script).actions;
87104
assert.deepEqual(parsed[0]?.positionals, ['./page.png']);
88105
assert.equal(parsed[0]?.flags.screenshotPixelDensity, 2);
89106
assert.equal(parsed[0]?.flags.screenshotFullscreen, true);
90-
assert.equal(parsed[0]?.flags.screenshotMaxSize, 1024);
107+
assert.equal(parsed[0]?.flags.screenshotScale, 0.3);
91108
assert.equal(parsed[0]?.flags.screenshotNoStabilize, true);
92109
});
93110

packages/ad-script/src/internal/script-utils.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -202,9 +202,6 @@ export function appendRecordActionScriptArgs(parts: string[], action: SessionAct
202202
if (typeof action.flags?.fps === 'number') {
203203
parts.push('--fps', String(action.flags.fps));
204204
}
205-
if (typeof action.flags?.screenshotMaxSize === 'number') {
206-
parts.push('--max-size', String(action.flags.screenshotMaxSize));
207-
}
208205
if (typeof action.flags?.quality === 'number' || typeof action.flags?.quality === 'string') {
209206
parts.push('--quality', String(action.flags.quality));
210207
}

0 commit comments

Comments
 (0)