@playdrop/playdrop-cli 0.13.6 → 0.13.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/config/client-meta.json +2 -2
- package/dist/apps/index.d.ts +1 -1
- package/dist/apps/index.js +1 -1
- package/dist/apps/loadCheck.d.ts +6 -6
- package/dist/apps/loadCheck.js +109 -199
- package/dist/apps/upload.d.ts +1 -1
- package/dist/apps/upload.js +2 -5
- package/dist/commands/check.js +16 -24
- package/dist/commands/upload.js +2 -8
- package/node_modules/@playdrop/config/client-meta.json +2 -2
- package/node_modules/@playdrop/config/dist/tsconfig.tsbuildinfo +1 -1
- package/node_modules/@playdrop/types/dist/version.d.ts +18 -1
- package/node_modules/@playdrop/types/dist/version.d.ts.map +1 -1
- package/package.json +1 -1
- package/dist/apps/playtestContract.d.ts +0 -2
- package/dist/apps/playtestContract.js +0 -77
package/config/client-meta.json
CHANGED
package/dist/apps/index.d.ts
CHANGED
|
@@ -26,7 +26,7 @@ export type AppPipelineOptions = {
|
|
|
26
26
|
agentTaskId?: number;
|
|
27
27
|
playtestProof?: AgentTaskPlaytestProof;
|
|
28
28
|
mediaCaptureRequired?: boolean;
|
|
29
|
-
|
|
29
|
+
playtestSmokeCheckRequired?: boolean;
|
|
30
30
|
captureSession?: TaskCaptureSession | null;
|
|
31
31
|
artifacts?: AppBuildArtifacts;
|
|
32
32
|
};
|
package/dist/apps/index.js
CHANGED
|
@@ -67,7 +67,7 @@ async function runAppPipeline(client, task, options) {
|
|
|
67
67
|
agentTaskId: options?.agentTaskId,
|
|
68
68
|
playtestProof: options?.playtestProof,
|
|
69
69
|
mediaCaptureRequired: options?.mediaCaptureRequired,
|
|
70
|
-
|
|
70
|
+
playtestSmokeCheckRequired: options?.playtestSmokeCheckRequired,
|
|
71
71
|
captureSession: options?.captureSession,
|
|
72
72
|
};
|
|
73
73
|
const upload = await (0, upload_1.uploadApp)(client, task, artifacts, uploadOptions);
|
package/dist/apps/loadCheck.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ApiClient } from '@playdrop/api-client';
|
|
2
|
-
import { type AppPlaytestTape, type AppSurface, type AppUploadCausalComparison, type AppUploadMachineCausalEvidence, type UserResponse } from '@playdrop/types';
|
|
2
|
+
import { type AppPlaytestTape, type AppPlaytestTapeEvent, type AppSurface, type AppUploadCausalComparison, type AppUploadMachineCausalEvidence, type UserResponse } from '@playdrop/types';
|
|
3
3
|
import type { BrowserContextOptions, Frame, Page } from 'playwright-core';
|
|
4
4
|
import type { AppTask } from '../catalogue';
|
|
5
5
|
import { type TaskCaptureSession } from '../appUrls';
|
|
@@ -52,7 +52,9 @@ export declare function focusProjectCheckGameFrame(page: Pick<Page, 'mouse'>, fr
|
|
|
52
52
|
width: number;
|
|
53
53
|
height: number;
|
|
54
54
|
}>;
|
|
55
|
-
export declare function dispatchPlaytestTapeToFrame(page: Page, frame: Frame, surface: AppSurface, tape: AppPlaytestTape
|
|
55
|
+
export declare function dispatchPlaytestTapeToFrame(page: Page, frame: Frame, surface: AppSurface, tape: AppPlaytestTape, hooks?: {
|
|
56
|
+
onBeforeEvent?: (eventIndex: number, event: AppPlaytestTapeEvent) => void;
|
|
57
|
+
}): Promise<void>;
|
|
56
58
|
export declare function dispatchProjectCheckActionsToFrame(page: Page, frame: Frame, actions: readonly ProjectCheckAction[]): Promise<void>;
|
|
57
59
|
export declare function formatHostedLoadCheckFailure(taskName: string, result: HostedLoadCheckResult, scope?: 'local' | 'staged' | 'final'): string;
|
|
58
60
|
export declare function redactHostedLoadCheckSecrets(text: string, sourceUrl: string): string;
|
|
@@ -89,17 +91,15 @@ export declare function runUploadedHostedLoadCheck(input: {
|
|
|
89
91
|
playtestTape?: AppPlaytestTape;
|
|
90
92
|
postReadyWaitMs?: number;
|
|
91
93
|
}): Promise<HostedLoadCheckResult>;
|
|
92
|
-
export declare function
|
|
93
|
-
export declare function compareCausalFrames(firstPath: string, secondPath: string, thresholds?: {
|
|
94
|
+
export declare function comparePlaytestFrames(firstPath: string, secondPath: string, thresholds?: {
|
|
94
95
|
meanDeltaMin: number;
|
|
95
96
|
changedPixelRatioMin: number;
|
|
96
97
|
}): Promise<AppUploadCausalComparison>;
|
|
97
|
-
export declare function
|
|
98
|
+
export declare function runUploadedHostedPlaytestSmokeCheck(input: {
|
|
98
99
|
client: ApiClient;
|
|
99
100
|
creatorUsername: string;
|
|
100
101
|
task: AppTask;
|
|
101
102
|
sessionId: string;
|
|
102
|
-
teachingText: string;
|
|
103
103
|
timeoutMs?: number;
|
|
104
104
|
token?: string | null;
|
|
105
105
|
currentUser?: UserResponse | null;
|
package/dist/apps/loadCheck.js
CHANGED
|
@@ -15,9 +15,8 @@ exports.formatHostedLoadCheckFailure = formatHostedLoadCheckFailure;
|
|
|
15
15
|
exports.redactHostedLoadCheckSecrets = redactHostedLoadCheckSecrets;
|
|
16
16
|
exports.runLocalHostedLoadCheck = runLocalHostedLoadCheck;
|
|
17
17
|
exports.runUploadedHostedLoadCheck = runUploadedHostedLoadCheck;
|
|
18
|
-
exports.
|
|
19
|
-
exports.
|
|
20
|
-
exports.runUploadedHostedCausalCheck = runUploadedHostedCausalCheck;
|
|
18
|
+
exports.comparePlaytestFrames = comparePlaytestFrames;
|
|
19
|
+
exports.runUploadedHostedPlaytestSmokeCheck = runUploadedHostedPlaytestSmokeCheck;
|
|
21
20
|
const types_1 = require("@playdrop/types");
|
|
22
21
|
const node_crypto_1 = require("node:crypto");
|
|
23
22
|
const promises_1 = require("node:fs/promises");
|
|
@@ -39,12 +38,8 @@ Object.defineProperty(exports, "PLAYDROP_SURFACE_CONTEXT_OPTIONS", { enumerable:
|
|
|
39
38
|
const FRAME_SELECTOR = 'iframe[title="Game"]';
|
|
40
39
|
const DEFAULT_HOSTED_LOAD_TIMEOUT_MS = 15000;
|
|
41
40
|
const POST_READY_SETTLE_MS = 750;
|
|
42
|
-
const
|
|
43
|
-
const
|
|
44
|
-
const CAUSAL_START_FRAME_MEAN_DELTA_MIN = 0.02;
|
|
45
|
-
const CAUSAL_START_FRAME_CHANGED_PIXEL_RATIO_MIN = 0.08;
|
|
46
|
-
const CAUSAL_PROGRESS_FRAME_MEAN_DELTA_MIN = 0.012;
|
|
47
|
-
const CAUSAL_PROGRESS_FRAME_CHANGED_PIXEL_RATIO_MIN = 0.05;
|
|
41
|
+
const PLAYTEST_INTERACTIVE_MEAN_DELTA_MIN = 0.002;
|
|
42
|
+
const PLAYTEST_INTERACTIVE_CHANGED_PIXEL_RATIO_MIN = 0.01;
|
|
48
43
|
const LOAD_CHECK_SURFACE_ORDER = ['DESKTOP', 'MOBILE_LANDSCAPE', 'MOBILE_PORTRAIT'];
|
|
49
44
|
const GOOGLE_TELEMETRY_HOSTS = [
|
|
50
45
|
'analytics.google.com',
|
|
@@ -370,78 +365,86 @@ async function dispatchTouchPoints(session, type, points) {
|
|
|
370
365
|
})),
|
|
371
366
|
});
|
|
372
367
|
}
|
|
373
|
-
async function dispatchPlaytestTapeToFrame(page, frame, surface, tape) {
|
|
368
|
+
async function dispatchPlaytestTapeToFrame(page, frame, surface, tape, hooks = {}) {
|
|
374
369
|
const box = await prepareProjectCheckGameFrame(frame);
|
|
375
370
|
const mobile = surface !== 'DESKTOP';
|
|
376
371
|
const session = mobile ? await page.context().newCDPSession(page) : null;
|
|
377
372
|
const touchPoints = new Map();
|
|
378
373
|
const startedAt = Date.now();
|
|
379
374
|
try {
|
|
380
|
-
for (const event of tape.events) {
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
await page.keyboard.down(event.key);
|
|
387
|
-
continue;
|
|
388
|
-
}
|
|
389
|
-
if (event.type === 'keyUp') {
|
|
390
|
-
await page.keyboard.up(event.key);
|
|
391
|
-
continue;
|
|
392
|
-
}
|
|
393
|
-
if (!('x' in event) || !('y' in event)) {
|
|
394
|
-
throw new Error(`project_check_playtest_event_unsupported:${event.type}`);
|
|
395
|
-
}
|
|
396
|
-
const point = resolveTapePoint(box, event);
|
|
397
|
-
const pointerId = 'pointerId' in event ? event.pointerId ?? 0 : 0;
|
|
398
|
-
if (event.type === 'tap') {
|
|
399
|
-
if (mobile && session) {
|
|
400
|
-
touchPoints.set(pointerId, point);
|
|
401
|
-
await dispatchTouchPoints(session, 'touchStart', touchPoints);
|
|
402
|
-
await page.waitForTimeout(event.durationMs ?? types_1.APP_PLAYTEST_DEFAULT_TAP_DURATION_MS);
|
|
403
|
-
touchPoints.delete(pointerId);
|
|
404
|
-
await dispatchTouchPoints(session, 'touchEnd', touchPoints);
|
|
375
|
+
for (const [eventIndex, event] of tape.events.entries()) {
|
|
376
|
+
hooks.onBeforeEvent?.(eventIndex, event);
|
|
377
|
+
try {
|
|
378
|
+
const waitMs = event.atMs - (Date.now() - startedAt);
|
|
379
|
+
if (waitMs > 0) {
|
|
380
|
+
await page.waitForTimeout(waitMs);
|
|
405
381
|
}
|
|
406
|
-
|
|
407
|
-
await page.
|
|
408
|
-
|
|
409
|
-
await page.waitForTimeout(event.durationMs ?? types_1.APP_PLAYTEST_DEFAULT_TAP_DURATION_MS);
|
|
410
|
-
await page.mouse.up({ button: 'left' });
|
|
382
|
+
if (event.type === 'keyDown') {
|
|
383
|
+
await page.keyboard.down(event.key);
|
|
384
|
+
continue;
|
|
411
385
|
}
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
if (mobile && session) {
|
|
416
|
-
touchPoints.set(pointerId, point);
|
|
417
|
-
await dispatchTouchPoints(session, 'touchStart', touchPoints);
|
|
386
|
+
if (event.type === 'keyUp') {
|
|
387
|
+
await page.keyboard.up(event.key);
|
|
388
|
+
continue;
|
|
418
389
|
}
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
await page.mouse.down({ button: 'left' });
|
|
390
|
+
if (!('x' in event) || !('y' in event)) {
|
|
391
|
+
throw new Error(`project_check_playtest_event_unsupported:${event.type}`);
|
|
422
392
|
}
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
393
|
+
const point = resolveTapePoint(box, event);
|
|
394
|
+
const pointerId = 'pointerId' in event ? event.pointerId ?? 0 : 0;
|
|
395
|
+
if (event.type === 'tap') {
|
|
396
|
+
if (mobile && session) {
|
|
397
|
+
touchPoints.set(pointerId, point);
|
|
398
|
+
await dispatchTouchPoints(session, 'touchStart', touchPoints);
|
|
399
|
+
await page.waitForTimeout(event.durationMs ?? types_1.APP_PLAYTEST_DEFAULT_TAP_DURATION_MS);
|
|
400
|
+
touchPoints.delete(pointerId);
|
|
401
|
+
await dispatchTouchPoints(session, 'touchEnd', touchPoints);
|
|
402
|
+
}
|
|
403
|
+
else {
|
|
404
|
+
await page.mouse.move(point.x, point.y);
|
|
405
|
+
await page.mouse.down({ button: 'left' });
|
|
406
|
+
await page.waitForTimeout(event.durationMs ?? types_1.APP_PLAYTEST_DEFAULT_TAP_DURATION_MS);
|
|
407
|
+
await page.mouse.up({ button: 'left' });
|
|
408
|
+
}
|
|
409
|
+
continue;
|
|
429
410
|
}
|
|
430
|
-
|
|
431
|
-
|
|
411
|
+
if (event.type === 'pointerDown') {
|
|
412
|
+
if (mobile && session) {
|
|
413
|
+
touchPoints.set(pointerId, point);
|
|
414
|
+
await dispatchTouchPoints(session, 'touchStart', touchPoints);
|
|
415
|
+
}
|
|
416
|
+
else {
|
|
417
|
+
await page.mouse.move(point.x, point.y);
|
|
418
|
+
await page.mouse.down({ button: 'left' });
|
|
419
|
+
}
|
|
420
|
+
continue;
|
|
432
421
|
}
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
422
|
+
if (event.type === 'pointerMove') {
|
|
423
|
+
if (mobile && session) {
|
|
424
|
+
touchPoints.set(pointerId, point);
|
|
425
|
+
await dispatchTouchPoints(session, 'touchMove', touchPoints);
|
|
426
|
+
}
|
|
427
|
+
else {
|
|
428
|
+
await page.mouse.move(point.x, point.y);
|
|
429
|
+
}
|
|
430
|
+
continue;
|
|
439
431
|
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
432
|
+
if (event.type === 'pointerUp') {
|
|
433
|
+
if (mobile && session) {
|
|
434
|
+
touchPoints.delete(pointerId);
|
|
435
|
+
await dispatchTouchPoints(session, 'touchEnd', touchPoints);
|
|
436
|
+
}
|
|
437
|
+
else {
|
|
438
|
+
await page.mouse.move(point.x, point.y);
|
|
439
|
+
await page.mouse.up({ button: 'left' });
|
|
440
|
+
}
|
|
443
441
|
}
|
|
444
442
|
}
|
|
443
|
+
catch (error) {
|
|
444
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
445
|
+
throw new Error(`project_check_playtest_action_failed: action ${eventIndex + 1} of ${tape.events.length} `
|
|
446
|
+
+ `(${event.type} at ${event.atMs} ms) failed: ${detail}`);
|
|
447
|
+
}
|
|
445
448
|
}
|
|
446
449
|
const remainingMs = tape.durationMs - (Date.now() - startedAt);
|
|
447
450
|
if (remainingMs > 0) {
|
|
@@ -581,6 +584,8 @@ async function runHostedLoadCheck(options) {
|
|
|
581
584
|
const shouldBootstrapSavedSession = options.savedSessionBootstrap !== false && hasExplicitToken;
|
|
582
585
|
const bootstrapToken = shouldBootstrapSavedSession ? options.token?.trim() || null : null;
|
|
583
586
|
const bootstrapUser = shouldBootstrapSavedSession ? options.user ?? null : null;
|
|
587
|
+
let currentTapeAction = '';
|
|
588
|
+
const withTapeAction = (line) => (currentTapeAction ? `${line} after ${currentTapeAction}` : line);
|
|
584
589
|
let hostedLaunchWaiterSettled = false;
|
|
585
590
|
let resolveHostedLaunchWaiter = null;
|
|
586
591
|
const hostedLaunchWaiter = new Promise((resolve) => {
|
|
@@ -672,7 +677,7 @@ async function runHostedLoadCheck(options) {
|
|
|
672
677
|
const type = message.type();
|
|
673
678
|
const args = await Promise.all(message.args().map((arg) => arg.jsonValue().catch(() => arg.toString())));
|
|
674
679
|
const rendered = args.length > 0 ? args.map(formatConsoleValue).join(' ') : message.text();
|
|
675
|
-
const line = redactReportText(`[check][console:${type}] ${rendered}`);
|
|
680
|
+
const line = redactReportText(withTapeAction(`[check][console:${type}] ${rendered}`));
|
|
676
681
|
if (type === 'error' || type === 'assert' || type === 'trace') {
|
|
677
682
|
errors.push(line);
|
|
678
683
|
console.error(line);
|
|
@@ -688,8 +693,9 @@ async function runHostedLoadCheck(options) {
|
|
|
688
693
|
});
|
|
689
694
|
page.on('pageerror', (error) => {
|
|
690
695
|
const text = redactReportText(error?.message ?? String(error));
|
|
691
|
-
|
|
692
|
-
|
|
696
|
+
const line = withTapeAction(`[check][pageerror] ${text}`);
|
|
697
|
+
errors.push(line);
|
|
698
|
+
console.error(line);
|
|
693
699
|
});
|
|
694
700
|
page.on('requestfailed', (request) => {
|
|
695
701
|
const failure = request.failure();
|
|
@@ -702,8 +708,9 @@ async function runHostedLoadCheck(options) {
|
|
|
702
708
|
if (/ERR_ABORTED/i.test(errorText) || /ERR_HTTP2_PROTOCOL_ERROR/i.test(errorText)) {
|
|
703
709
|
return;
|
|
704
710
|
}
|
|
705
|
-
|
|
706
|
-
|
|
711
|
+
const line = withTapeAction(`[check][requestfailed] ${text}`);
|
|
712
|
+
errors.push(line);
|
|
713
|
+
console.error(line);
|
|
707
714
|
});
|
|
708
715
|
page.on('response', (response) => {
|
|
709
716
|
const status = response.status();
|
|
@@ -715,8 +722,9 @@ async function runHostedLoadCheck(options) {
|
|
|
715
722
|
warnings.push(`[check][response] ${text}`);
|
|
716
723
|
return;
|
|
717
724
|
}
|
|
718
|
-
|
|
719
|
-
|
|
725
|
+
const line = withTapeAction(`[check][response] ${text}`);
|
|
726
|
+
errors.push(line);
|
|
727
|
+
console.error(line);
|
|
720
728
|
});
|
|
721
729
|
console.log(`[check] Opening ${reportTargetUrl}`);
|
|
722
730
|
const response = await page.goto(options.targetUrl, { waitUntil: 'domcontentloaded', timeout: 30000 });
|
|
@@ -768,7 +776,11 @@ async function runHostedLoadCheck(options) {
|
|
|
768
776
|
if (!options.playtestSurface) {
|
|
769
777
|
throw new Error('project_check_playtest_surface_required');
|
|
770
778
|
}
|
|
771
|
-
await dispatchPlaytestTapeToFrame(page, frame, options.playtestSurface, options.playtestTape
|
|
779
|
+
await dispatchPlaytestTapeToFrame(page, frame, options.playtestSurface, options.playtestTape, {
|
|
780
|
+
onBeforeEvent: (eventIndex, event) => {
|
|
781
|
+
currentTapeAction = `action ${eventIndex + 1} of ${options.playtestTape.events.length} (${event.type} at ${event.atMs} ms)`;
|
|
782
|
+
},
|
|
783
|
+
});
|
|
772
784
|
}
|
|
773
785
|
else if (options.actions?.length) {
|
|
774
786
|
await dispatchProjectCheckActionsToFrame(page, frame, options.actions);
|
|
@@ -1082,55 +1094,12 @@ async function runUploadedHostedLoadCheck(input) {
|
|
|
1082
1094
|
artifactFingerprint: recordedLaunchCheck.artifactFingerprint,
|
|
1083
1095
|
};
|
|
1084
1096
|
}
|
|
1085
|
-
function sliceFirstPrimaryVerbAction(tape) {
|
|
1086
|
-
const prefix = tape.events.slice(0, tape.startOnlyEventCount);
|
|
1087
|
-
const postStartEvents = tape.events.slice(tape.startOnlyEventCount);
|
|
1088
|
-
let actionEventCount = 0;
|
|
1089
|
-
if (tape.primaryVerb === 'tap') {
|
|
1090
|
-
actionEventCount = 1;
|
|
1091
|
-
}
|
|
1092
|
-
else if (tape.primaryVerb === 'key') {
|
|
1093
|
-
const activeKeys = new Set();
|
|
1094
|
-
for (let index = 0; index < postStartEvents.length; index += 1) {
|
|
1095
|
-
const event = postStartEvents[index];
|
|
1096
|
-
if (event.type === 'keyDown')
|
|
1097
|
-
activeKeys.add(event.key);
|
|
1098
|
-
if (event.type === 'keyUp')
|
|
1099
|
-
activeKeys.delete(event.key);
|
|
1100
|
-
if (activeKeys.size === 0 && index > 0) {
|
|
1101
|
-
actionEventCount = index + 1;
|
|
1102
|
-
break;
|
|
1103
|
-
}
|
|
1104
|
-
}
|
|
1105
|
-
}
|
|
1106
|
-
else {
|
|
1107
|
-
const activePointers = new Set();
|
|
1108
|
-
for (let index = 0; index < postStartEvents.length; index += 1) {
|
|
1109
|
-
const event = postStartEvents[index];
|
|
1110
|
-
if (event.type === 'pointerDown')
|
|
1111
|
-
activePointers.add(event.pointerId ?? 0);
|
|
1112
|
-
if (event.type === 'pointerUp')
|
|
1113
|
-
activePointers.delete(event.pointerId ?? 0);
|
|
1114
|
-
if (activePointers.size === 0 && index > 0) {
|
|
1115
|
-
actionEventCount = index + 1;
|
|
1116
|
-
break;
|
|
1117
|
-
}
|
|
1118
|
-
}
|
|
1119
|
-
}
|
|
1120
|
-
if (actionEventCount <= 0) {
|
|
1121
|
-
throw new Error(`agent_task_machine_causal_gate_invalid_tape:first_${tape.primaryVerb}_action_incomplete`);
|
|
1122
|
-
}
|
|
1123
|
-
return {
|
|
1124
|
-
...tape,
|
|
1125
|
-
events: [...prefix, ...postStartEvents.slice(0, actionEventCount)],
|
|
1126
|
-
};
|
|
1127
|
-
}
|
|
1128
1097
|
async function hashFile(path) {
|
|
1129
1098
|
return (0, node_crypto_1.createHash)('sha256').update(await (0, promises_1.readFile)(path)).digest('hex');
|
|
1130
1099
|
}
|
|
1131
|
-
async function
|
|
1132
|
-
meanDeltaMin:
|
|
1133
|
-
changedPixelRatioMin:
|
|
1100
|
+
async function comparePlaytestFrames(firstPath, secondPath, thresholds = {
|
|
1101
|
+
meanDeltaMin: PLAYTEST_INTERACTIVE_MEAN_DELTA_MIN,
|
|
1102
|
+
changedPixelRatioMin: PLAYTEST_INTERACTIVE_CHANGED_PIXEL_RATIO_MIN,
|
|
1134
1103
|
}) {
|
|
1135
1104
|
const readPixels = async (path) => {
|
|
1136
1105
|
return await (0, sharp_1.default)(path)
|
|
@@ -1141,7 +1110,7 @@ async function compareCausalFrames(firstPath, secondPath, thresholds = {
|
|
|
1141
1110
|
};
|
|
1142
1111
|
const [first, second] = await Promise.all([readPixels(firstPath), readPixels(secondPath)]);
|
|
1143
1112
|
if (first.length !== second.length || first.length === 0) {
|
|
1144
|
-
throw new Error('
|
|
1113
|
+
throw new Error('playtest_smoke_check_frame_shape_mismatch: idle and tape screenshots have different dimensions');
|
|
1145
1114
|
}
|
|
1146
1115
|
let deltaTotal = 0;
|
|
1147
1116
|
let changedPixels = 0;
|
|
@@ -1159,30 +1128,19 @@ async function compareCausalFrames(firstPath, secondPath, thresholds = {
|
|
|
1159
1128
|
passed: meanDelta >= thresholds.meanDeltaMin || changedPixelRatio >= thresholds.changedPixelRatioMin,
|
|
1160
1129
|
};
|
|
1161
1130
|
}
|
|
1162
|
-
async function
|
|
1131
|
+
async function runUploadedHostedPlaytestSmokeCheck(input) {
|
|
1163
1132
|
const primarySurface = input.task.primarySurface;
|
|
1164
1133
|
const fullTape = primarySurface ? input.task.playtestTapes?.[primarySurface] : null;
|
|
1165
1134
|
if (!primarySurface || !fullTape) {
|
|
1166
|
-
throw new Error('
|
|
1135
|
+
throw new Error('agent_task_playtest_smoke_check_primary_tape_required');
|
|
1167
1136
|
}
|
|
1168
|
-
|
|
1169
|
-
throw new Error('agent_task_machine_causal_gate_start_only_required');
|
|
1170
|
-
}
|
|
1171
|
-
const startOnlyTape = {
|
|
1172
|
-
...fullTape,
|
|
1173
|
-
events: fullTape.events.slice(0, fullTape.startOnlyEventCount),
|
|
1174
|
-
};
|
|
1175
|
-
const firstActionTape = sliceFirstPrimaryVerbAction(fullTape);
|
|
1176
|
-
const tempDir = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'playdrop-causal-gate-'));
|
|
1137
|
+
const tempDir = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'playdrop-playtest-smoke-'));
|
|
1177
1138
|
const paths = {
|
|
1178
1139
|
zeroInput: (0, node_path_1.join)(tempDir, 'zero-input.png'),
|
|
1179
|
-
startOnlyReady: (0, node_path_1.join)(tempDir, 'start-only-ready.png'),
|
|
1180
|
-
startOnly: (0, node_path_1.join)(tempDir, 'start-only.png'),
|
|
1181
|
-
firstAction: (0, node_path_1.join)(tempDir, 'first-action.png'),
|
|
1182
1140
|
fullTape: (0, node_path_1.join)(tempDir, 'full-tape.png'),
|
|
1183
1141
|
};
|
|
1184
1142
|
const run = async (label, options) => {
|
|
1185
|
-
console.log(`[upload]
|
|
1143
|
+
console.log(`[upload] Playtest smoke check: ${label} from sdk.host.ready().`);
|
|
1186
1144
|
const result = await runUploadedHostedLoadCheck({
|
|
1187
1145
|
client: input.client,
|
|
1188
1146
|
creatorUsername: input.creatorUsername,
|
|
@@ -1195,10 +1153,11 @@ async function runUploadedHostedCausalCheck(input) {
|
|
|
1195
1153
|
...options,
|
|
1196
1154
|
});
|
|
1197
1155
|
if (result.status !== 'PASSED') {
|
|
1198
|
-
throw new Error(`
|
|
1156
|
+
throw new Error(`agent_task_playtest_smoke_check_runtime_failed: ${label} failed. `
|
|
1157
|
+
+ formatHostedLoadCheckFailure(input.task.name, result, 'staged'));
|
|
1199
1158
|
}
|
|
1200
1159
|
if (!result.artifactFingerprint) {
|
|
1201
|
-
throw new Error('
|
|
1160
|
+
throw new Error('agent_task_playtest_smoke_check_artifact_fingerprint_missing');
|
|
1202
1161
|
}
|
|
1203
1162
|
return result;
|
|
1204
1163
|
};
|
|
@@ -1207,87 +1166,38 @@ async function runUploadedHostedCausalCheck(input) {
|
|
|
1207
1166
|
postReadyWaitMs: fullTape.durationMs,
|
|
1208
1167
|
gameFrameScreenshotPath: paths.zeroInput,
|
|
1209
1168
|
});
|
|
1210
|
-
const startOnly = await run('start-only', {
|
|
1211
|
-
playtestTape: startOnlyTape,
|
|
1212
|
-
readyGameFrameScreenshotPath: paths.startOnlyReady,
|
|
1213
|
-
gameFrameScreenshotPath: paths.startOnly,
|
|
1214
|
-
});
|
|
1215
|
-
const firstAction = await run('first-action', {
|
|
1216
|
-
playtestTape: firstActionTape,
|
|
1217
|
-
gameFrameScreenshotPath: paths.firstAction,
|
|
1218
|
-
});
|
|
1219
1169
|
const full = await run('full-tape', {
|
|
1220
1170
|
playtestTape: fullTape,
|
|
1221
1171
|
gameFrameScreenshotPath: paths.fullTape,
|
|
1222
1172
|
});
|
|
1223
1173
|
const artifactFingerprint = full.artifactFingerprint;
|
|
1224
|
-
if (
|
|
1225
|
-
throw new Error('
|
|
1226
|
-
}
|
|
1227
|
-
const comparisons = {
|
|
1228
|
-
startOnlyVsReady: await compareCausalFrames(paths.startOnlyReady, paths.startOnly, {
|
|
1229
|
-
meanDeltaMin: CAUSAL_START_FRAME_MEAN_DELTA_MIN,
|
|
1230
|
-
changedPixelRatioMin: CAUSAL_START_FRAME_CHANGED_PIXEL_RATIO_MIN,
|
|
1231
|
-
}),
|
|
1232
|
-
startOnlyVsZeroInput: await compareCausalFrames(paths.zeroInput, paths.startOnly, {
|
|
1233
|
-
meanDeltaMin: CAUSAL_START_FRAME_MEAN_DELTA_MIN,
|
|
1234
|
-
changedPixelRatioMin: CAUSAL_START_FRAME_CHANGED_PIXEL_RATIO_MIN,
|
|
1235
|
-
}),
|
|
1236
|
-
fullTapeVsZeroInput: await compareCausalFrames(paths.zeroInput, paths.fullTape, {
|
|
1237
|
-
meanDeltaMin: CAUSAL_PROGRESS_FRAME_MEAN_DELTA_MIN,
|
|
1238
|
-
changedPixelRatioMin: CAUSAL_PROGRESS_FRAME_CHANGED_PIXEL_RATIO_MIN,
|
|
1239
|
-
}),
|
|
1240
|
-
fullTapeVsStartOnly: await compareCausalFrames(paths.startOnly, paths.fullTape, {
|
|
1241
|
-
meanDeltaMin: CAUSAL_PROGRESS_FRAME_MEAN_DELTA_MIN,
|
|
1242
|
-
changedPixelRatioMin: CAUSAL_PROGRESS_FRAME_CHANGED_PIXEL_RATIO_MIN,
|
|
1243
|
-
}),
|
|
1244
|
-
fullTapeVsFirstAction: await compareCausalFrames(paths.firstAction, paths.fullTape, {
|
|
1245
|
-
meanDeltaMin: CAUSAL_PROGRESS_FRAME_MEAN_DELTA_MIN,
|
|
1246
|
-
changedPixelRatioMin: CAUSAL_PROGRESS_FRAME_CHANGED_PIXEL_RATIO_MIN,
|
|
1247
|
-
}),
|
|
1248
|
-
};
|
|
1249
|
-
if (!comparisons.startOnlyVsReady.passed || !comparisons.startOnlyVsZeroInput.passed) {
|
|
1250
|
-
throw new Error(`agent_task_machine_causal_gate_failed:start-only did not provably leave the title state. `
|
|
1251
|
-
+ `ready(mean=${comparisons.startOnlyVsReady.meanDelta.toFixed(4)},changed=${comparisons.startOnlyVsReady.changedPixelRatio.toFixed(4)}); `
|
|
1252
|
-
+ `zero(mean=${comparisons.startOnlyVsZeroInput.meanDelta.toFixed(4)},changed=${comparisons.startOnlyVsZeroInput.changedPixelRatio.toFixed(4)}). `
|
|
1253
|
-
+ 'Fix the start action, visible start-only outcome, or ready-relative timing.');
|
|
1254
|
-
}
|
|
1255
|
-
if (!comparisons.fullTapeVsZeroInput.passed || !comparisons.fullTapeVsStartOnly.passed) {
|
|
1256
|
-
throw new Error(`agent_task_machine_causal_gate_failed:full tape did not meaningfully beat zero-input and start-only controls. `
|
|
1257
|
-
+ `zero(mean=${comparisons.fullTapeVsZeroInput.meanDelta.toFixed(4)},changed=${comparisons.fullTapeVsZeroInput.changedPixelRatio.toFixed(4)}); `
|
|
1258
|
-
+ `start(mean=${comparisons.fullTapeVsStartOnly.meanDelta.toFixed(4)},changed=${comparisons.fullTapeVsStartOnly.changedPixelRatio.toFixed(4)}). `
|
|
1259
|
-
+ 'Fix the game or tape before upload.');
|
|
1174
|
+
if (zeroInput.artifactFingerprint !== artifactFingerprint) {
|
|
1175
|
+
throw new Error('agent_task_playtest_smoke_check_artifact_changed_during_check');
|
|
1260
1176
|
}
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
+
|
|
1177
|
+
const comparison = await comparePlaytestFrames(paths.zeroInput, paths.fullTape);
|
|
1178
|
+
if (!comparison.passed) {
|
|
1179
|
+
throw new Error(`agent_task_playtest_smoke_check_not_interactive: The validator completed all ${fullTape.events.length} tape actions `
|
|
1180
|
+
+ `without a crash, but the result looked the same as running the game for ${fullTape.durationMs} ms without input `
|
|
1181
|
+
+ `(mean pixel change ${comparison.meanDelta.toFixed(4)}, changed pixels ${(comparison.changedPixelRatio * 100).toFixed(2)}%). `
|
|
1182
|
+
+ 'Make sure the tape performs the core gameplay action and produces a visible response, then rerun the tape check.');
|
|
1265
1183
|
}
|
|
1266
|
-
const teachingText = input.teachingText.trim();
|
|
1267
1184
|
const evidence = {
|
|
1268
|
-
version:
|
|
1185
|
+
version: 2,
|
|
1269
1186
|
source: 'CLI_MACHINE',
|
|
1270
1187
|
artifactFingerprint,
|
|
1271
1188
|
primarySurface,
|
|
1272
1189
|
primaryVerb: fullTape.primaryVerb,
|
|
1273
1190
|
runs: {
|
|
1274
1191
|
zeroInput: { finalFrameSha256: await hashFile(paths.zeroInput) },
|
|
1275
|
-
startOnly: {
|
|
1276
|
-
readyFrameSha256: await hashFile(paths.startOnlyReady),
|
|
1277
|
-
finalFrameSha256: await hashFile(paths.startOnly),
|
|
1278
|
-
},
|
|
1279
|
-
firstAction: { finalFrameSha256: await hashFile(paths.firstAction) },
|
|
1280
1192
|
fullTape: { finalFrameSha256: await hashFile(paths.fullTape) },
|
|
1281
1193
|
},
|
|
1282
|
-
|
|
1283
|
-
teachingText,
|
|
1284
|
-
teachingTextSha256: (0, node_crypto_1.createHash)('sha256').update(teachingText).digest('hex'),
|
|
1194
|
+
comparison,
|
|
1285
1195
|
};
|
|
1286
1196
|
const recorded = await input.client.recordAppUploadCausalCheck(input.creatorUsername, input.task.name, input.sessionId, { evidence });
|
|
1287
1197
|
if (recorded.session.causalCheck.status !== 'PASSED') {
|
|
1288
|
-
throw new Error('
|
|
1198
|
+
throw new Error('agent_task_playtest_smoke_check_not_recorded');
|
|
1289
1199
|
}
|
|
1290
|
-
console.log(`[upload]
|
|
1200
|
+
console.log(`[upload] Playtest smoke check passed for ${input.creatorUsername}/${input.task.name}.`);
|
|
1291
1201
|
return evidence;
|
|
1292
1202
|
}
|
|
1293
1203
|
finally {
|
package/dist/apps/upload.d.ts
CHANGED
|
@@ -23,7 +23,7 @@ export type AppUploadOptions = {
|
|
|
23
23
|
agentTaskId?: number;
|
|
24
24
|
playtestProof?: AgentTaskPlaytestProof;
|
|
25
25
|
mediaCaptureRequired?: boolean;
|
|
26
|
-
|
|
26
|
+
playtestSmokeCheckRequired?: boolean;
|
|
27
27
|
captureSession?: TaskCaptureSession | null;
|
|
28
28
|
};
|
|
29
29
|
export declare function uploadApp(client: ApiClient, task: AppTask, artifacts: AppBuildArtifacts | null, options?: AppUploadOptions): Promise<AppUploadResult>;
|
package/dist/apps/upload.js
CHANGED
|
@@ -9,7 +9,6 @@ const node_path_1 = require("node:path");
|
|
|
9
9
|
const http_1 = require("../http");
|
|
10
10
|
const loadCheck_1 = require("./loadCheck");
|
|
11
11
|
const listingPreflight_1 = require("../listingPreflight");
|
|
12
|
-
const playtestContract_1 = require("./playtestContract");
|
|
13
12
|
const EXTENSION_TO_MIME = {
|
|
14
13
|
'.png': 'image/png',
|
|
15
14
|
'.jpg': 'image/jpeg',
|
|
@@ -355,14 +354,12 @@ async function uploadAppVersion(client, task, artifacts, options) {
|
|
|
355
354
|
}
|
|
356
355
|
if (task.hostingMode !== 'EXTERNAL'
|
|
357
356
|
&& !task.externalUrl
|
|
358
|
-
&& options?.
|
|
359
|
-
|
|
360
|
-
await (0, loadCheck_1.runUploadedHostedCausalCheck)({
|
|
357
|
+
&& options?.playtestSmokeCheckRequired) {
|
|
358
|
+
await (0, loadCheck_1.runUploadedHostedPlaytestSmokeCheck)({
|
|
361
359
|
client,
|
|
362
360
|
creatorUsername,
|
|
363
361
|
task,
|
|
364
362
|
sessionId,
|
|
365
|
-
teachingText,
|
|
366
363
|
timeoutMs: options.loadCheckTimeoutMs,
|
|
367
364
|
token: options?.token ?? null,
|
|
368
365
|
currentUser: options?.user ?? null,
|
package/dist/commands/check.js
CHANGED
|
@@ -222,7 +222,6 @@ async function check(targetArg, options = {}) {
|
|
|
222
222
|
});
|
|
223
223
|
if (tapeSurface && playtestTape) {
|
|
224
224
|
const idleScreenshotPath = buildComparisonScreenshotPath(screenshotPath, 'idle');
|
|
225
|
-
const startOnlyScreenshotPath = buildComparisonScreenshotPath(screenshotPath, 'start-only');
|
|
226
225
|
const tapeScreenshotPath = buildComparisonScreenshotPath(screenshotPath, 'tape');
|
|
227
226
|
console.log(`[check] Running ${playtestTape.durationMs} ms zero-input baseline on ${tapeSurface}.`);
|
|
228
227
|
const idleResult = await runCheck({
|
|
@@ -235,22 +234,6 @@ async function check(targetArg, options = {}) {
|
|
|
235
234
|
process.exitCode = 1;
|
|
236
235
|
return;
|
|
237
236
|
}
|
|
238
|
-
console.log(`[check] Reopening a clean run and executing only the first ${playtestTape.startOnlyEventCount} startup event(s).`);
|
|
239
|
-
const startOnlyTape = playtestTape.startOnlyEventCount > 0
|
|
240
|
-
? { ...playtestTape, events: playtestTape.events.slice(0, playtestTape.startOnlyEventCount) }
|
|
241
|
-
: null;
|
|
242
|
-
const startOnlyResult = await runCheck({
|
|
243
|
-
screenshotPath: startOnlyScreenshotPath,
|
|
244
|
-
surface: tapeSurface,
|
|
245
|
-
...(startOnlyTape
|
|
246
|
-
? { tape: startOnlyTape }
|
|
247
|
-
: { postReadyWaitMs: playtestTape.durationMs }),
|
|
248
|
-
});
|
|
249
|
-
if (startOnlyResult.status !== 'PASSED') {
|
|
250
|
-
(0, messages_1.printErrorWithHelp)((0, loadCheck_1.formatHostedLoadCheckFailure)(appName, startOnlyResult, 'local'), startOnlyResult.screenshotPath ? [`Start-only screenshot: ${startOnlyResult.screenshotPath}`] : [], { command: 'project check' });
|
|
251
|
-
process.exitCode = 1;
|
|
252
|
-
return;
|
|
253
|
-
}
|
|
254
237
|
console.log(`[check] Reopening a clean run and executing the ${tapeSurface} tape.`);
|
|
255
238
|
const tapeResult = await runCheck({
|
|
256
239
|
screenshotPath: tapeScreenshotPath,
|
|
@@ -262,14 +245,23 @@ async function check(targetArg, options = {}) {
|
|
|
262
245
|
process.exitCode = 1;
|
|
263
246
|
return;
|
|
264
247
|
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
248
|
+
const idleCapture = idleResult.screenshotPath ?? idleScreenshotPath;
|
|
249
|
+
const tapeCapture = tapeResult.screenshotPath ?? tapeScreenshotPath;
|
|
250
|
+
const comparison = await (0, loadCheck_1.comparePlaytestFrames)(idleCapture, tapeCapture);
|
|
251
|
+
if (!comparison.passed) {
|
|
252
|
+
(0, messages_1.printErrorWithHelp)(`playtest_smoke_check_not_interactive: The validator completed all ${playtestTape.events.length} tape actions `
|
|
253
|
+
+ `without a crash, but the result looked the same as running the game for ${playtestTape.durationMs} ms without input.`, [
|
|
254
|
+
`Idle screenshot: ${idleCapture}`,
|
|
255
|
+
`Tape screenshot: ${tapeCapture}`,
|
|
256
|
+
'Make sure the tape performs the core gameplay action and produces a visible response.',
|
|
257
|
+
], { command: 'project check' });
|
|
258
|
+
process.exitCode = 1;
|
|
259
|
+
return;
|
|
272
260
|
}
|
|
261
|
+
console.log(`[check] Playtest smoke check passed for ${currentUsername}/${appName}.`);
|
|
262
|
+
console.log(`[check] ${playtestTape.events.length} tape actions completed without a crash and produced a visible response.`);
|
|
263
|
+
console.log(`[check] Idle: ${idleCapture}`);
|
|
264
|
+
console.log(`[check] Tape: ${tapeCapture}`);
|
|
273
265
|
return;
|
|
274
266
|
}
|
|
275
267
|
const result = await runCheck({ screenshotPath });
|