@pixel-point/toolcraft 0.0.16 → 0.0.17
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/package.json +1 -1
- package/src/generate-performance-iteration-execution.test.mjs +2 -2
- package/src/generate-test-demand-only-lifecycle.mjs +14 -3
- package/src/generate-test-dependency-sandbox.mjs +42 -5
- package/src/generate-test-doc-assertions.mjs +68 -18
- package/src/generate-test-doc-assertions.test.mjs +22 -0
- package/src/generate-test-file-assertions.mjs +43 -76
- package/src/generate-test-output-assertions.mjs +95 -0
- package/src/generate-test-package-assertions.mjs +6 -0
- package/src/generate-test-performance-boundary-assertions.mjs +45 -0
- package/src/generate-test-product-fixture.mjs +1 -0
- package/src/generate-test-product-fixture.test.mjs +18 -0
- package/src/generate-test-product-iteration.mjs +1 -0
- package/src/generate-test-product-iteration.test.mjs +1 -0
- package/src/generate-test-standalone-delivery-stages.mjs +14 -6
- package/src/generate.test.mjs +14 -0
- package/templates/runtime/contracts/component-contracts.choices.test.ts +12 -9
- package/templates/runtime/contracts/component-contracts.choices.ts +10 -9
- package/templates/runtime/contracts/component-contracts.runtime-acceptance.test.ts +17 -2
- package/templates/runtime/contracts/component-contracts.runtime-panels.test.ts +1 -1
- package/templates/runtime/contracts/component-contracts.runtime-shell.test.ts +1 -1
- package/templates/runtime/contracts/component-contracts.runtime.ts +3 -2
- package/templates/runtime/contracts/decision-contracts.test.ts +25 -4
- package/templates/runtime/contracts/decision-contracts.ts +3 -3
- package/templates/runtime/contracts/performance-verification-policy.test.ts +59 -6
- package/templates/runtime/contracts/performance-verification-policy.ts +5 -2
- package/templates/runtime/export/artifact-download.test.ts +71 -0
- package/templates/runtime/export/artifact-download.ts +80 -0
- package/templates/runtime/export/artifact-export-request.ts +23 -0
- package/templates/runtime/export/artifact-export-settings.test.ts +75 -0
- package/templates/runtime/export/artifact-export-settings.ts +96 -0
- package/templates/runtime/export/artifact-frame-renderer.test.ts +107 -0
- package/templates/runtime/export/artifact-frame-renderer.ts +96 -0
- package/templates/runtime/export/artifact-frame-state.test.ts +54 -0
- package/templates/runtime/export/artifact-frame-state.ts +44 -0
- package/templates/runtime/export/artifact-scene-frame.test.ts +84 -0
- package/templates/runtime/export/artifact-scene-frame.ts +118 -0
- package/templates/runtime/export/export-background.ts +65 -0
- package/templates/runtime/export/export-error.ts +43 -0
- package/templates/runtime/export/export.test.ts +6 -186
- package/templates/runtime/export/image-artifact-export.test.ts +111 -0
- package/templates/runtime/export/image-artifact-export.ts +145 -0
- package/templates/runtime/export/index.ts +14 -1
- package/templates/runtime/export/product-export-renderer.test.ts +77 -0
- package/templates/runtime/export/product-export-renderer.ts +63 -0
- package/templates/runtime/export/video-artifact-export.test.ts +166 -0
- package/templates/runtime/export/video-artifact-export.ts +164 -0
- package/templates/runtime/export/video-encoding-backend.ts +124 -0
- package/templates/runtime/export/video-encoding-policy.test.ts +68 -0
- package/templates/runtime/export/video-encoding-policy.ts +86 -0
- package/templates/runtime/export/video-frame-schedule.test.ts +58 -0
- package/templates/runtime/export/video-frame-schedule.ts +42 -0
- package/templates/runtime/index.ts +1 -0
- package/templates/runtime/react/app-shell/runtime-public-api-runtime.test.tsx +291 -0
- package/templates/runtime/react/app-shell/runtime-public-api-test-support.ts +21 -0
- package/templates/runtime/react/app-shell/runtime-public-api.test.tsx +4 -303
- package/templates/runtime/react/app-shell/toolcraft-app.scene-export.test.tsx +12 -27
- package/templates/runtime/react/app-shell/toolcraft-app.tsx +22 -4
- package/templates/runtime/react/app-shell/toolcraft-source-asset-context.test.tsx +16 -57
- package/templates/runtime/react/app-shell/toolcraft-source-asset-context.tsx +75 -434
- package/templates/runtime/react/app-shell/toolcraft-source-asset-owner.test.ts +194 -0
- package/templates/runtime/react/app-shell/toolcraft-source-asset-owner.ts +227 -0
- package/templates/runtime/react/app-shell/toolcraft-source-asset-provider-ownership.test.tsx +243 -0
- package/templates/runtime/react/app-shell/toolcraft-source-asset-test-support.ts +51 -0
- package/templates/runtime/react/app-shell/toolcraft-unavailable-resource-proof.test.ts +138 -0
- package/templates/runtime/react/app-shell/toolcraft-unavailable-resource-proof.ts +378 -0
- package/templates/runtime/react/controls-panel/__tests__/controls-panel.footer-actions.test.tsx +7 -12
- package/templates/runtime/react/controls-panel/__tests__/controls-panel.scene-export-actions.test.tsx +17 -83
- package/templates/runtime/react/controls-panel/actions/controls-panel-actions.ts +42 -84
- package/templates/runtime/react/controls-panel/actions/export-action-runner.ts +62 -0
- package/templates/runtime/react/panel-host/panel-host-runtime-placement.test.tsx +30 -0
- package/templates/runtime/react/panel-host/panel-host.tsx +4 -0
- package/templates/runtime/scene/scene-bounds.ts +1 -2
- package/templates/runtime/schema/canvas-render-scale.ts +62 -0
- package/templates/runtime/schema/controls-panel-section-id.test.ts +1 -1
- package/templates/runtime/schema/controls-panel-section-roundtrip.test.ts +3 -3
- package/templates/runtime/schema/define-toolcraft.setup-background.test.ts +1 -1
- package/templates/runtime/schema/define-toolcraft.setup-render-scale-timeline.test.ts +71 -3
- package/templates/runtime/schema/define-toolcraft.ts +1 -1
- package/templates/runtime/schema/schema-resolvers.ts +0 -56
- package/templates/runtime/schema/types.ts +4 -4
- package/templates/runtime/state/canvas-background-state.ts +6 -0
- package/templates/runtime/testing/performance-render-plan-integration.test.ts +64 -0
- package/templates/runtime/testing/performance-renderer-pipeline-validation.ts +18 -1
- package/templates/starter/AGENTS.md +8 -7
- package/templates/starter/NOTICE.md +6 -0
- package/templates/starter/docs/toolcraft/README.md +2 -1
- package/templates/starter/docs/toolcraft/acceptance-testing.md +8 -6
- package/templates/starter/docs/toolcraft/agent-worklog.md +3 -3
- package/templates/starter/docs/toolcraft/assembly-workflow.md +4 -3
- package/templates/starter/docs/toolcraft/core/media-upload.md +2 -2
- package/templates/starter/docs/toolcraft/core/performance.md +14 -3
- package/templates/starter/docs/toolcraft/core/runtime-boundary.md +3 -1
- package/templates/starter/docs/toolcraft/core/setup-export.md +16 -12
- package/templates/starter/docs/toolcraft/core/timeline-animation.md +4 -4
- package/templates/starter/docs/toolcraft/performance.md +20 -6
- package/templates/starter/docs/toolcraft/renderer-technique.md +1 -1
- package/templates/starter/docs/toolcraft/schema-reference.md +2 -6
- package/templates/starter/docs/toolcraft/workflow.md +7 -2
- package/templates/starter/e2e/app-browser-control-coverage.spec.ts +33 -0
- package/templates/starter/e2e/app-browser-infinity-canvas-evidence.spec.ts +43 -1
- package/templates/starter/e2e/app-browser-render-scale-evidence.spec.ts +60 -6
- package/templates/starter/e2e/app-browser-render-scale-requirements.spec.ts +77 -1
- package/templates/starter/e2e/app-browser-runtime-provenance.spec.ts +8 -0
- package/templates/starter/e2e/app-browser-runtime-reporter.spec.ts +35 -0
- package/templates/starter/e2e/app-browser-runtime-requirements.spec.ts +4 -1
- package/templates/starter/e2e/app-browser-semantic-evidence.spec.ts +0 -1
- package/templates/starter/e2e/app-browser-shell.spec.ts +13 -5
- package/templates/starter/e2e/app-performance-path-adapters.ts +8 -1
- package/templates/starter/e2e/app-performance.spec.ts +16 -9
- package/templates/starter/e2e/browser-infinity-canvas-evidence.ts +9 -330
- package/templates/starter/e2e/browser-infinity-canvas-unavailable-image-evidence.ts +370 -0
- package/templates/starter/e2e/browser-media-export-evidence.spec.ts +77 -0
- package/templates/starter/e2e/browser-media-export-evidence.ts +208 -0
- package/templates/starter/e2e/browser-render-scale-evidence.ts +37 -29
- package/templates/starter/e2e/browser-runtime-evidence-reporter.ts +4 -1
- package/templates/starter/e2e/browser-runtime-evidence-requirements.ts +43 -9
- package/templates/starter/e2e/browser-standard-timeline-evidence.ts +106 -0
- package/templates/starter/e2e/decoded-pixel-observation.spec.ts +74 -0
- package/templates/starter/e2e/decoded-pixel-observation.ts +227 -0
- package/templates/starter/e2e/export-artifact-helpers.ts +206 -25
- package/templates/starter/e2e/image-artifact-inspection.spec.ts +41 -0
- package/templates/starter/e2e/image-artifact-inspection.ts +99 -0
- package/templates/starter/e2e/performance-canvas-helpers.ts +22 -21
- package/templates/starter/e2e/performance-canvas-quality-guard.spec.ts +312 -0
- package/templates/starter/e2e/performance-canvas-quality-guard.ts +196 -0
- package/templates/starter/e2e/performance-canvas-quality-observer.ts +351 -0
- package/templates/starter/e2e/performance-control-layout-helpers.ts +2 -2
- package/templates/starter/e2e/performance-path-adapter-contract.ts +4 -4
- package/templates/starter/e2e/performance-path-helpers.spec.ts +203 -70
- package/templates/starter/e2e/performance-path-helpers.ts +210 -91
- package/templates/starter/e2e/performance-pipeline-evidence-continuity.spec.ts +76 -0
- package/templates/starter/e2e/performance-pipeline-evidence-test-fixtures.ts +11 -2
- package/templates/starter/e2e/performance-pipeline-invariants.ts +47 -4
- package/templates/starter/e2e/performance-render-scale-runner.spec.ts +140 -38
- package/templates/starter/e2e/video-artifact-inspection.spec.ts +64 -0
- package/templates/starter/e2e/video-artifact-inspection.ts +281 -0
- package/templates/starter/gitignore +1 -0
- package/templates/starter/package.json +1 -0
- package/templates/starter/scripts/check-toolcraft-docs.mjs +17 -0
- package/templates/starter/scripts/toolcraft-delivery-plan-authority.test.mjs +60 -0
- package/templates/starter/scripts/toolcraft-delivery-plan-errors.test.mjs +7 -2
- package/templates/starter/scripts/toolcraft-delivery-plan.mjs +7 -3
- package/templates/starter/scripts/toolcraft-product-boundary-ast.mjs +24 -2
- package/templates/starter/scripts/toolcraft-product-boundary.mjs +12 -0
- package/templates/starter/scripts/toolcraft-product-boundary.test.mjs +46 -0
- package/templates/starter/scripts/toolcraft-product-evidence-boundary.test.mjs +85 -20
- package/templates/starter/scripts/toolcraft-product-evidence-import-policy.mjs +43 -0
- package/templates/starter/scripts/toolcraft-product-export-boundary.mjs +168 -0
- package/templates/starter/scripts/toolcraft-typescript-analysis.d.mts +73 -0
- package/templates/starter/scripts/toolcraft-unavailable-resource-evidence-facade-test-helpers.mjs +318 -0
- package/templates/starter/scripts/toolcraft-unavailable-resource-evidence-facade.test.mjs +155 -206
- package/templates/starter/scripts/toolcraft-verification-inventory.mjs +1 -0
- package/templates/starter/scripts/toolcraft-verification-receipt.test.mjs +51 -0
- package/templates/starter/scripts/toolcraft-vite-env-activation.test.mjs +128 -59
- package/templates/starter/src/app/acceptance/export-artifact-coverage.ts +113 -0
- package/templates/starter/src/app/acceptance/output-background-rules.ts +1 -1
- package/templates/starter/src/app/acceptance/output-export.ts +13 -2
- package/templates/starter/src/app/acceptance/types.ts +7 -0
- package/templates/starter/src/app/acceptance/validate-coverage.ts +2 -1
- package/templates/starter/src/app/starter-acceptance.export-artifact.test.ts +131 -0
- package/templates/starter/src/app/starter-acceptance.keyframes.test.ts +14 -4
- package/templates/starter/src/app/starter-acceptance.output-export-rules.test.ts +42 -1
- package/templates/starter/src/app/starter-acceptance.ts +2 -0
- package/templates/starter/src/app/starter-performance-test-utils.ts +103 -0
- package/templates/starter/src/app/starter-performance.gates.test.ts +231 -278
- package/templates/starter/src/app/starter-performance.lifecycle.test.ts +261 -0
- package/templates/starter/src/app/test-evidence/browser-runtime-contract.ts +2 -0
- package/templates/starter/src/routes/index.tsx +1 -0
- package/templates/runtime/export/export.ts +0 -166
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { expect, test, type Page, type TestInfo } from "@playwright/test";
|
|
2
2
|
import {
|
|
3
|
+
defineToolcraft,
|
|
3
4
|
defineToolcraftPerformance,
|
|
4
5
|
deriveToolcraftPerformancePaths,
|
|
5
6
|
registerToolcraftRendererPipeline,
|
|
6
7
|
type ToolcraftRendererPipelinePassContract,
|
|
7
8
|
} from "@repo/toolcraft-runtime";
|
|
9
|
+
import type {
|
|
10
|
+
ToolcraftPerformancePipelinePhase,
|
|
11
|
+
} from "../src/app/test-evidence/browser-performance-contract";
|
|
8
12
|
|
|
9
|
-
import { starterSchema } from "../src/app/starter-schema";
|
|
10
13
|
import { attachedEvidenceTypes } from "./browser-semantic-evidence-test-helpers";
|
|
11
14
|
import {
|
|
12
15
|
compileToolcraftPerformancePathAdapterMatrix,
|
|
@@ -18,6 +21,54 @@ import {
|
|
|
18
21
|
} from "./performance-fixture-selection";
|
|
19
22
|
|
|
20
23
|
const canvasSelector = "[data-render-scale-canvas]";
|
|
24
|
+
const originalFixtureResolutionMode =
|
|
25
|
+
process.env[TOOLCRAFT_PERFORMANCE_FIXTURE_RESOLUTION_MODE_ENV];
|
|
26
|
+
const originalRequestAuthorityHash =
|
|
27
|
+
process.env[TOOLCRAFT_PERFORMANCE_REQUEST_AUTHORITY_HASH_ENV];
|
|
28
|
+
|
|
29
|
+
test.beforeEach(() => {
|
|
30
|
+
process.env[TOOLCRAFT_PERFORMANCE_FIXTURE_RESOLUTION_MODE_ENV] =
|
|
31
|
+
"full-certification";
|
|
32
|
+
delete process.env[TOOLCRAFT_PERFORMANCE_REQUEST_AUTHORITY_HASH_ENV];
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test.afterEach(() => {
|
|
36
|
+
if (originalFixtureResolutionMode === undefined) {
|
|
37
|
+
delete process.env[TOOLCRAFT_PERFORMANCE_FIXTURE_RESOLUTION_MODE_ENV];
|
|
38
|
+
} else {
|
|
39
|
+
process.env[TOOLCRAFT_PERFORMANCE_FIXTURE_RESOLUTION_MODE_ENV] =
|
|
40
|
+
originalFixtureResolutionMode;
|
|
41
|
+
}
|
|
42
|
+
if (originalRequestAuthorityHash === undefined) {
|
|
43
|
+
delete process.env[TOOLCRAFT_PERFORMANCE_REQUEST_AUTHORITY_HASH_ENV];
|
|
44
|
+
} else {
|
|
45
|
+
process.env[TOOLCRAFT_PERFORMANCE_REQUEST_AUTHORITY_HASH_ENV] =
|
|
46
|
+
originalRequestAuthorityHash;
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const renderScaleSchema = defineToolcraft({
|
|
51
|
+
canvas: { enabled: true, renderScale: true },
|
|
52
|
+
panels: {
|
|
53
|
+
controls: {
|
|
54
|
+
sections: [
|
|
55
|
+
{
|
|
56
|
+
controls: {
|
|
57
|
+
color: {
|
|
58
|
+
defaultValue: "#ffffff",
|
|
59
|
+
label: "Color",
|
|
60
|
+
performanceRole: "responsiveness",
|
|
61
|
+
target: "appearance.color",
|
|
62
|
+
type: "color",
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
title: "Appearance",
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
title: "Controls",
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
});
|
|
21
72
|
|
|
22
73
|
type RenderScalePipelinePasses = {
|
|
23
74
|
composite: ToolcraftRendererPipelinePassContract<void>;
|
|
@@ -27,10 +78,10 @@ const renderScaleRegistration =
|
|
|
27
78
|
registerToolcraftRendererPipeline<RenderScalePipelinePasses>()({
|
|
28
79
|
interactionInvalidation: [
|
|
29
80
|
{
|
|
30
|
-
interaction: "control-
|
|
81
|
+
interaction: "control-drag",
|
|
31
82
|
invalidates: ["composite"],
|
|
32
83
|
mustNotInvalidate: [],
|
|
33
|
-
targets: ["
|
|
84
|
+
targets: ["appearance.color"],
|
|
34
85
|
},
|
|
35
86
|
],
|
|
36
87
|
passes: [
|
|
@@ -41,8 +92,8 @@ const renderScaleRegistration =
|
|
|
41
92
|
relationship: "constant",
|
|
42
93
|
},
|
|
43
94
|
id: "composite",
|
|
44
|
-
inputs: ["
|
|
45
|
-
invalidatedBy: ["
|
|
95
|
+
inputs: ["appearance.color"],
|
|
96
|
+
invalidatedBy: ["appearance.color"],
|
|
46
97
|
kind: "composite",
|
|
47
98
|
output: "preview",
|
|
48
99
|
quality: "full",
|
|
@@ -62,7 +113,7 @@ const renderScalePerformance = defineToolcraftPerformance({
|
|
|
62
113
|
});
|
|
63
114
|
|
|
64
115
|
const [renderScalePath] = deriveToolcraftPerformancePaths(
|
|
65
|
-
|
|
116
|
+
renderScaleSchema,
|
|
66
117
|
renderScalePerformance,
|
|
67
118
|
);
|
|
68
119
|
|
|
@@ -121,15 +172,17 @@ async function installFixture(page: Page): Promise<void> {
|
|
|
121
172
|
}, renderScaleRegistration.runtimeId);
|
|
122
173
|
}
|
|
123
174
|
|
|
175
|
+
type PhaseScale = Readonly<Record<ToolcraftPerformancePipelinePhase, number>>;
|
|
176
|
+
|
|
124
177
|
function compileRunner(
|
|
125
|
-
|
|
178
|
+
backingScaleByPhase: PhaseScale,
|
|
126
179
|
prepare: () => Promise<void> = async () => undefined,
|
|
180
|
+
afterAction?: (page: Page) => Promise<void>,
|
|
127
181
|
) {
|
|
128
|
-
return compileToolcraftPerformancePathAdapterMatrix(
|
|
129
|
-
[
|
|
130
|
-
[
|
|
182
|
+
return compileToolcraftPerformancePathAdapterMatrix({
|
|
183
|
+
adapters: [
|
|
131
184
|
{
|
|
132
|
-
action: async ({ page }) => {
|
|
185
|
+
action: async ({ page, phase }) => {
|
|
133
186
|
await page.evaluate(({ scale, selector }) => {
|
|
134
187
|
const state = Reflect.get(
|
|
135
188
|
globalThis,
|
|
@@ -164,7 +217,8 @@ function compileRunner(
|
|
|
164
217
|
canvas.height = Math.round(
|
|
165
218
|
rect.height * window.devicePixelRatio * scale,
|
|
166
219
|
);
|
|
167
|
-
}, { scale:
|
|
220
|
+
}, { scale: backingScaleByPhase[phase], selector: canvasSelector });
|
|
221
|
+
await afterAction?.(page);
|
|
168
222
|
},
|
|
169
223
|
observeOutcome: ({ page }) =>
|
|
170
224
|
page.evaluate(
|
|
@@ -178,15 +232,54 @@ function compileRunner(
|
|
|
178
232
|
),
|
|
179
233
|
pathId: renderScalePath!.id,
|
|
180
234
|
prepare,
|
|
181
|
-
renderScaleBacking: {
|
|
182
|
-
canvasSelector,
|
|
183
|
-
selectedScale: 2,
|
|
184
|
-
},
|
|
185
235
|
},
|
|
186
236
|
],
|
|
187
|
-
|
|
237
|
+
canvasBacking: { canvasSelector },
|
|
238
|
+
paths: [renderScalePath!],
|
|
239
|
+
schema: renderScaleSchema,
|
|
240
|
+
})[0]!;
|
|
188
241
|
}
|
|
189
242
|
|
|
243
|
+
test("runner preserves an undefined phase rejection when guard disposal fails", async ({
|
|
244
|
+
page,
|
|
245
|
+
}) => {
|
|
246
|
+
await installFixture(page);
|
|
247
|
+
const outcome = await runToolcraftPerformancePath(
|
|
248
|
+
page,
|
|
249
|
+
renderScaleSchema,
|
|
250
|
+
renderScalePerformance,
|
|
251
|
+
compileRunner(
|
|
252
|
+
{ cold: 2, sustained: 2, warm: 2 },
|
|
253
|
+
async () => undefined,
|
|
254
|
+
async (currentPage) => {
|
|
255
|
+
await currentPage.evaluate(() => {
|
|
256
|
+
const stateKey = Object.getOwnPropertyNames(globalThis).find((key) =>
|
|
257
|
+
key.startsWith("__toolcraftCanvasQualityObserver"),
|
|
258
|
+
);
|
|
259
|
+
if (!stateKey) {
|
|
260
|
+
throw new Error("Quality guard fixture could not find observer state.");
|
|
261
|
+
}
|
|
262
|
+
const state = Reflect.get(globalThis, stateKey) as {
|
|
263
|
+
flush: () => Promise<void>;
|
|
264
|
+
};
|
|
265
|
+
Reflect.set(globalThis, stateKey, {
|
|
266
|
+
dispose: () => {
|
|
267
|
+
throw new Error("forced guard disposal failure");
|
|
268
|
+
},
|
|
269
|
+
flush: state.flush,
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
return Promise.reject(undefined);
|
|
273
|
+
},
|
|
274
|
+
),
|
|
275
|
+
).then(
|
|
276
|
+
() => ({ status: "resolved" as const }),
|
|
277
|
+
(reason: unknown) => ({ reason, status: "rejected" as const }),
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
expect(outcome).toEqual({ reason: undefined, status: "rejected" });
|
|
281
|
+
});
|
|
282
|
+
|
|
190
283
|
test("requestless performance runner rejects before adapter operations", async ({
|
|
191
284
|
page,
|
|
192
285
|
}) => {
|
|
@@ -202,9 +295,9 @@ test("requestless performance runner rejects before adapter operations", async (
|
|
|
202
295
|
await expect(
|
|
203
296
|
runToolcraftPerformancePath(
|
|
204
297
|
page,
|
|
205
|
-
|
|
298
|
+
renderScaleSchema,
|
|
206
299
|
renderScalePerformance,
|
|
207
|
-
compileRunner(2, async () => {
|
|
300
|
+
compileRunner({ cold: 2, sustained: 2, warm: 2 }, async () => {
|
|
208
301
|
prepareCount += 1;
|
|
209
302
|
}),
|
|
210
303
|
),
|
|
@@ -234,9 +327,9 @@ test("browser perf: runner proves real render-scale backing", async ({
|
|
|
234
327
|
|
|
235
328
|
await runToolcraftPerformancePath(
|
|
236
329
|
page,
|
|
237
|
-
|
|
330
|
+
renderScaleSchema,
|
|
238
331
|
renderScalePerformance,
|
|
239
|
-
compileRunner(2),
|
|
332
|
+
compileRunner({ cold: 2, sustained: 2, warm: 2 }),
|
|
240
333
|
);
|
|
241
334
|
|
|
242
335
|
expect(
|
|
@@ -246,21 +339,30 @@ test("browser perf: runner proves real render-scale backing", async ({
|
|
|
246
339
|
).toHaveLength(3);
|
|
247
340
|
});
|
|
248
341
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
342
|
+
for (const failedPhase of ["cold", "warm", "sustained"] as const) {
|
|
343
|
+
test(`browser perf: runner rejects a ${failedPhase} render-scale quality clamp`, async ({
|
|
344
|
+
page,
|
|
345
|
+
}, testInfo: TestInfo) => {
|
|
346
|
+
await installFixture(page);
|
|
347
|
+
const attachmentCount = testInfo.attachments.length;
|
|
348
|
+
const scaleByPhase: PhaseScale = { cold: 2, sustained: 2, warm: 2 };
|
|
254
349
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
350
|
+
await expect(
|
|
351
|
+
runToolcraftPerformancePath(
|
|
352
|
+
page,
|
|
353
|
+
renderScaleSchema,
|
|
354
|
+
renderScalePerformance,
|
|
355
|
+
compileRunner({ ...scaleByPhase, [failedPhase]: 1 }),
|
|
356
|
+
),
|
|
357
|
+
).rejects.toThrow(new RegExp(`${failedPhase}.*backing width`, "iu"));
|
|
358
|
+
const attachedQuality = testInfo.attachments
|
|
359
|
+
.slice(attachmentCount)
|
|
360
|
+
.filter((attachment) =>
|
|
361
|
+
attachment.body?.toString().includes("performance-render-scale"),
|
|
362
|
+
)
|
|
363
|
+
.map((attachment) => attachment.body?.toString() ?? "");
|
|
364
|
+
expect(attachedQuality).not.toContainEqual(
|
|
365
|
+
expect.stringContaining(`${renderScalePath!.id}#${failedPhase}`),
|
|
366
|
+
);
|
|
367
|
+
});
|
|
368
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { expect, test } from "@playwright/test";
|
|
2
|
+
|
|
3
|
+
import { assertToolcraftVideoPacketSchedule } from "./video-artifact-inspection";
|
|
4
|
+
|
|
5
|
+
const schedule = Object.freeze([
|
|
6
|
+
Object.freeze({ durationSeconds: 1 / 30, index: 0, timeSeconds: 0 }),
|
|
7
|
+
Object.freeze({ durationSeconds: 1 / 30, index: 1, timeSeconds: 1 / 30 }),
|
|
8
|
+
]);
|
|
9
|
+
|
|
10
|
+
test("accepts real packet timings that match the runtime schedule", () => {
|
|
11
|
+
expect(() =>
|
|
12
|
+
assertToolcraftVideoPacketSchedule({
|
|
13
|
+
packetTimings: schedule,
|
|
14
|
+
schedule,
|
|
15
|
+
timeResolution: 90_000,
|
|
16
|
+
}),
|
|
17
|
+
).not.toThrow();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("rejects synthetic or dropped packet counts", () => {
|
|
21
|
+
expect(() =>
|
|
22
|
+
assertToolcraftVideoPacketSchedule({
|
|
23
|
+
packetTimings: schedule.slice(0, 1),
|
|
24
|
+
schedule,
|
|
25
|
+
timeResolution: 90_000,
|
|
26
|
+
}),
|
|
27
|
+
).toThrow("contains 1 packets; runtime scheduled 2");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("rejects packet timestamps that do not match runtime time", () => {
|
|
31
|
+
expect(() =>
|
|
32
|
+
assertToolcraftVideoPacketSchedule({
|
|
33
|
+
packetTimings: [
|
|
34
|
+
schedule[0]!,
|
|
35
|
+
{ durationSeconds: 1 / 30, timeSeconds: 2 / 30 },
|
|
36
|
+
],
|
|
37
|
+
schedule,
|
|
38
|
+
timeResolution: 90_000,
|
|
39
|
+
}),
|
|
40
|
+
).toThrow("packet 1 timestamp");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("rejects packet durations that do not match runtime cadence", () => {
|
|
44
|
+
expect(() =>
|
|
45
|
+
assertToolcraftVideoPacketSchedule({
|
|
46
|
+
packetTimings: [
|
|
47
|
+
schedule[0]!,
|
|
48
|
+
{ durationSeconds: 2 / 30, timeSeconds: 1 / 30 },
|
|
49
|
+
],
|
|
50
|
+
schedule,
|
|
51
|
+
timeResolution: 90_000,
|
|
52
|
+
}),
|
|
53
|
+
).toThrow("packet 1 duration");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("rejects missing timing authority", () => {
|
|
57
|
+
expect(() =>
|
|
58
|
+
assertToolcraftVideoPacketSchedule({
|
|
59
|
+
packetTimings: schedule,
|
|
60
|
+
schedule,
|
|
61
|
+
timeResolution: 0,
|
|
62
|
+
}),
|
|
63
|
+
).toThrow("positive time resolution");
|
|
64
|
+
});
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
|
|
3
|
+
import type { Download, Page } from "@playwright/test";
|
|
4
|
+
import {
|
|
5
|
+
ALL_FORMATS,
|
|
6
|
+
BlobSource,
|
|
7
|
+
EncodedPacketSink,
|
|
8
|
+
Input,
|
|
9
|
+
} from "mediabunny";
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
observeToolcraftDecodedPixels,
|
|
13
|
+
type ToolcraftDecodedPixelObservation,
|
|
14
|
+
} from "./decoded-pixel-observation";
|
|
15
|
+
import type {
|
|
16
|
+
ToolcraftVideoArtifactInspection,
|
|
17
|
+
ToolcraftVideoPacketTiming,
|
|
18
|
+
} from "./export-artifact-helpers";
|
|
19
|
+
|
|
20
|
+
export type ToolcraftScheduledVideoFrame = Readonly<{
|
|
21
|
+
durationSeconds: number;
|
|
22
|
+
index: number;
|
|
23
|
+
timeSeconds: number;
|
|
24
|
+
}>;
|
|
25
|
+
|
|
26
|
+
export function assertToolcraftVideoPacketSchedule({
|
|
27
|
+
packetTimings,
|
|
28
|
+
schedule,
|
|
29
|
+
timeResolution,
|
|
30
|
+
}: Readonly<{
|
|
31
|
+
packetTimings: readonly ToolcraftVideoPacketTiming[];
|
|
32
|
+
schedule: readonly ToolcraftScheduledVideoFrame[];
|
|
33
|
+
timeResolution: number;
|
|
34
|
+
}>): void {
|
|
35
|
+
if (!Number.isFinite(timeResolution) || timeResolution <= 0) {
|
|
36
|
+
throw new Error("Video packet proof requires a positive time resolution.");
|
|
37
|
+
}
|
|
38
|
+
if (packetTimings.length !== schedule.length) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
`Encoded video contains ${packetTimings.length} packets; runtime scheduled ${schedule.length}.`,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const toleranceSeconds = 1 / timeResolution;
|
|
45
|
+
for (const [index, actual] of packetTimings.entries()) {
|
|
46
|
+
const expected = schedule[index];
|
|
47
|
+
if (!expected) {
|
|
48
|
+
throw new Error(`Runtime schedule is missing frame ${index}.`);
|
|
49
|
+
}
|
|
50
|
+
if (
|
|
51
|
+
!Number.isFinite(actual.timeSeconds) ||
|
|
52
|
+
actual.timeSeconds < 0 ||
|
|
53
|
+
!Number.isFinite(actual.durationSeconds) ||
|
|
54
|
+
actual.durationSeconds <= 0
|
|
55
|
+
) {
|
|
56
|
+
throw new Error(`Encoded packet ${index} has invalid timing metadata.`);
|
|
57
|
+
}
|
|
58
|
+
if (
|
|
59
|
+
Math.abs(actual.timeSeconds - expected.timeSeconds) > toleranceSeconds
|
|
60
|
+
) {
|
|
61
|
+
throw new Error(
|
|
62
|
+
`Encoded packet ${index} timestamp does not match the runtime schedule.`,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
if (
|
|
66
|
+
Math.abs(actual.durationSeconds - expected.durationSeconds) >
|
|
67
|
+
toleranceSeconds
|
|
68
|
+
) {
|
|
69
|
+
throw new Error(
|
|
70
|
+
`Encoded packet ${index} duration does not match the runtime schedule.`,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export type ToolcraftInspectedVideoArtifact = Readonly<{
|
|
77
|
+
inspection: ToolcraftVideoArtifactInspection;
|
|
78
|
+
observations: readonly ToolcraftDecodedPixelObservation[];
|
|
79
|
+
sampleTimes: readonly number[];
|
|
80
|
+
timeResolution: number;
|
|
81
|
+
}>;
|
|
82
|
+
|
|
83
|
+
function detectVideoMediaType(bytes: Uint8Array): "video/mp4" | "video/webm" {
|
|
84
|
+
if (
|
|
85
|
+
bytes.length >= 8 &&
|
|
86
|
+
String.fromCharCode(...bytes.subarray(4, 8)) === "ftyp"
|
|
87
|
+
) {
|
|
88
|
+
return "video/mp4";
|
|
89
|
+
}
|
|
90
|
+
if (
|
|
91
|
+
bytes.length >= 4 &&
|
|
92
|
+
bytes[0] === 0x1a &&
|
|
93
|
+
bytes[1] === 0x45 &&
|
|
94
|
+
bytes[2] === 0xdf &&
|
|
95
|
+
bytes[3] === 0xa3
|
|
96
|
+
) {
|
|
97
|
+
return "video/webm";
|
|
98
|
+
}
|
|
99
|
+
throw new Error("Downloaded artifact is not a supported MP4 or WebM video.");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function getSampleTimes(
|
|
103
|
+
schedule: readonly ToolcraftScheduledVideoFrame[],
|
|
104
|
+
): readonly number[] {
|
|
105
|
+
if (schedule.length === 0) {
|
|
106
|
+
throw new Error("Video inspection requires a non-empty runtime frame schedule.");
|
|
107
|
+
}
|
|
108
|
+
const indices = [...new Set([0, Math.floor(schedule.length / 2), schedule.length - 1])];
|
|
109
|
+
return Object.freeze(indices.map((index) => schedule[index]!.timeSeconds));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function decodeSamplesInPage({
|
|
113
|
+
bytes,
|
|
114
|
+
mediaType,
|
|
115
|
+
page,
|
|
116
|
+
sampleTimes,
|
|
117
|
+
}: Readonly<{
|
|
118
|
+
bytes: Uint8Array;
|
|
119
|
+
mediaType: "video/mp4" | "video/webm";
|
|
120
|
+
page: Page;
|
|
121
|
+
sampleTimes: readonly number[];
|
|
122
|
+
}>): Promise<readonly Uint8ClampedArray[]> {
|
|
123
|
+
const samples = await page.evaluate(
|
|
124
|
+
async ({ base64, mediaType: decodedMediaType, times }) => {
|
|
125
|
+
const binary = atob(base64);
|
|
126
|
+
const data = Uint8Array.from(binary, (character) =>
|
|
127
|
+
character.charCodeAt(0),
|
|
128
|
+
);
|
|
129
|
+
const video = document.createElement("video");
|
|
130
|
+
video.muted = true;
|
|
131
|
+
video.playsInline = true;
|
|
132
|
+
video.preload = "auto";
|
|
133
|
+
const objectUrl = URL.createObjectURL(
|
|
134
|
+
new Blob([data], { type: decodedMediaType }),
|
|
135
|
+
);
|
|
136
|
+
video.src = objectUrl;
|
|
137
|
+
try {
|
|
138
|
+
await new Promise<void>((resolve, reject) => {
|
|
139
|
+
video.addEventListener("loadeddata", () => resolve(), { once: true });
|
|
140
|
+
video.addEventListener(
|
|
141
|
+
"error",
|
|
142
|
+
() => reject(new Error("Browser could not decode exported video.")),
|
|
143
|
+
{ once: true },
|
|
144
|
+
);
|
|
145
|
+
video.load();
|
|
146
|
+
});
|
|
147
|
+
const canvas = document.createElement("canvas");
|
|
148
|
+
canvas.width = 64;
|
|
149
|
+
canvas.height = 64;
|
|
150
|
+
const context = canvas.getContext("2d", { willReadFrequently: true });
|
|
151
|
+
if (!context) throw new Error("Unable to inspect exported video pixels.");
|
|
152
|
+
const decodedSamples: number[][] = [];
|
|
153
|
+
for (const timeSeconds of times) {
|
|
154
|
+
if (Math.abs(video.currentTime - timeSeconds) > 1e-6) {
|
|
155
|
+
await new Promise<void>((resolve, reject) => {
|
|
156
|
+
video.addEventListener("seeked", () => resolve(), { once: true });
|
|
157
|
+
video.addEventListener(
|
|
158
|
+
"error",
|
|
159
|
+
() => reject(new Error("Browser could not seek exported video.")),
|
|
160
|
+
{ once: true },
|
|
161
|
+
);
|
|
162
|
+
video.currentTime = timeSeconds;
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
context.clearRect(0, 0, 64, 64);
|
|
166
|
+
context.drawImage(video, 0, 0, 64, 64);
|
|
167
|
+
decodedSamples.push(
|
|
168
|
+
Array.from(context.getImageData(0, 0, 64, 64).data),
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
return decodedSamples;
|
|
172
|
+
} finally {
|
|
173
|
+
video.removeAttribute("src");
|
|
174
|
+
video.load();
|
|
175
|
+
URL.revokeObjectURL(objectUrl);
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
base64: Buffer.from(bytes).toString("base64"),
|
|
180
|
+
mediaType,
|
|
181
|
+
times: sampleTimes,
|
|
182
|
+
},
|
|
183
|
+
);
|
|
184
|
+
return Object.freeze(samples.map((sample) => Uint8ClampedArray.from(sample)));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export async function inspectToolcraftVideoDownload({
|
|
188
|
+
backgroundRgba,
|
|
189
|
+
download,
|
|
190
|
+
page,
|
|
191
|
+
schedule,
|
|
192
|
+
}: Readonly<{
|
|
193
|
+
backgroundRgba: readonly [number, number, number, number];
|
|
194
|
+
download: Download;
|
|
195
|
+
page: Page;
|
|
196
|
+
schedule: readonly ToolcraftScheduledVideoFrame[];
|
|
197
|
+
}>): Promise<ToolcraftInspectedVideoArtifact> {
|
|
198
|
+
const downloadPath = await download.path();
|
|
199
|
+
if (!downloadPath) {
|
|
200
|
+
throw new Error("Playwright did not expose the downloaded video path.");
|
|
201
|
+
}
|
|
202
|
+
const bytes = await fs.readFile(downloadPath);
|
|
203
|
+
if (bytes.byteLength === 0) {
|
|
204
|
+
throw new Error("Downloaded video artifact is empty.");
|
|
205
|
+
}
|
|
206
|
+
const mediaType = detectVideoMediaType(bytes);
|
|
207
|
+
const input = new Input({
|
|
208
|
+
formats: ALL_FORMATS,
|
|
209
|
+
source: new BlobSource(new Blob([bytes], { type: mediaType })),
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
let durationSeconds: number;
|
|
213
|
+
let height: number;
|
|
214
|
+
let packetTimings: ToolcraftVideoPacketTiming[];
|
|
215
|
+
let timeResolution: number;
|
|
216
|
+
let width: number;
|
|
217
|
+
try {
|
|
218
|
+
const tracks = await input.getVideoTracks();
|
|
219
|
+
const track = tracks[0];
|
|
220
|
+
if (!track) throw new Error("Exported video does not contain a video track.");
|
|
221
|
+
[durationSeconds, height, timeResolution, width] = await Promise.all([
|
|
222
|
+
input.computeDuration([track]),
|
|
223
|
+
track.getCodedHeight(),
|
|
224
|
+
track.getTimeResolution(),
|
|
225
|
+
track.getCodedWidth(),
|
|
226
|
+
]);
|
|
227
|
+
packetTimings = [];
|
|
228
|
+
const sink = new EncodedPacketSink(track);
|
|
229
|
+
for await (const packet of sink.packets(undefined, undefined, {
|
|
230
|
+
metadataOnly: true,
|
|
231
|
+
})) {
|
|
232
|
+
packetTimings.push(
|
|
233
|
+
Object.freeze({
|
|
234
|
+
durationSeconds: packet.duration,
|
|
235
|
+
timeSeconds: packet.timestamp,
|
|
236
|
+
}),
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
} finally {
|
|
240
|
+
input.dispose();
|
|
241
|
+
}
|
|
242
|
+
packetTimings.sort((left, right) => left.timeSeconds - right.timeSeconds);
|
|
243
|
+
|
|
244
|
+
const sampleTimes = getSampleTimes(schedule);
|
|
245
|
+
const decodedSamples = await decodeSamplesInPage({
|
|
246
|
+
bytes,
|
|
247
|
+
mediaType,
|
|
248
|
+
page,
|
|
249
|
+
sampleTimes,
|
|
250
|
+
});
|
|
251
|
+
const observations = await Promise.all(
|
|
252
|
+
decodedSamples.map((pixels) =>
|
|
253
|
+
observeToolcraftDecodedPixels({
|
|
254
|
+
backgroundRgba,
|
|
255
|
+
pixels,
|
|
256
|
+
sourceHeight: 64,
|
|
257
|
+
sourceWidth: 64,
|
|
258
|
+
}),
|
|
259
|
+
),
|
|
260
|
+
);
|
|
261
|
+
const inspection: ToolcraftVideoArtifactInspection = Object.freeze({
|
|
262
|
+
byteLength: bytes.byteLength,
|
|
263
|
+
durationMs: durationSeconds * 1000,
|
|
264
|
+
frameCount: packetTimings.length,
|
|
265
|
+
height,
|
|
266
|
+
kind: "video",
|
|
267
|
+
mediaType,
|
|
268
|
+
packetTimings: Object.freeze(packetTimings),
|
|
269
|
+
samplePixelHashes: Object.freeze(
|
|
270
|
+
observations.map(({ decodedPixelHash }) => decodedPixelHash),
|
|
271
|
+
),
|
|
272
|
+
width,
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
return Object.freeze({
|
|
276
|
+
inspection,
|
|
277
|
+
observations: Object.freeze(observations),
|
|
278
|
+
sampleTimes,
|
|
279
|
+
timeResolution,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
@@ -78,6 +78,15 @@ const forbiddenTextPatterns = [
|
|
|
78
78
|
const forbiddenModelContractPatterns = [
|
|
79
79
|
/\bgeometry-only (?:3D )?model(?:s| bundle| bundles| app| apps| default)?\b/iu,
|
|
80
80
|
];
|
|
81
|
+
const forbiddenPerformanceRoutingPatterns = [
|
|
82
|
+
/for a performance complaint only,\s+an exact request quote and the canonical affected performance path ids/iu,
|
|
83
|
+
/(?:\*\*)?performance complaint(?::\*\*|\.\*\*)[^\n]*(?:canonical affected path ids|targeted iteration)/iu,
|
|
84
|
+
/\ba performance complaint records[^\n]*(?:canonical affected path ids|targeted iteration)/iu,
|
|
85
|
+
/unresolved\s+`?needs-agent-judgment`?[^\n]*(?:creates neither|records neither|no `?performance intent)/iu,
|
|
86
|
+
/for unresolved\s+`?needs-agent-judgment`?[^\n]*no `?performance intent/iu,
|
|
87
|
+
/direct reports that the app lags[^\n]*select one performance iteration/iu,
|
|
88
|
+
/independent complaint still selects an iteration/iu,
|
|
89
|
+
];
|
|
81
90
|
|
|
82
91
|
async function readText(relativePath) {
|
|
83
92
|
return fs.readFile(path.join(projectRoot, relativePath), "utf8");
|
|
@@ -172,6 +181,14 @@ for (const pattern of forbiddenModelContractPatterns) {
|
|
|
172
181
|
}
|
|
173
182
|
}
|
|
174
183
|
|
|
184
|
+
for (const pattern of forbiddenPerformanceRoutingPatterns) {
|
|
185
|
+
if (pattern.test(combinedSource)) {
|
|
186
|
+
failures.push(
|
|
187
|
+
`local Toolcraft docs contain unconditional complaint routing ${pattern}`,
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
175
192
|
for (const term of [
|
|
176
193
|
"bounded ZIP",
|
|
177
194
|
"Blender-compatible fallback",
|