@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.
Files changed (170) hide show
  1. package/package.json +1 -1
  2. package/src/generate-performance-iteration-execution.test.mjs +2 -2
  3. package/src/generate-test-demand-only-lifecycle.mjs +14 -3
  4. package/src/generate-test-dependency-sandbox.mjs +42 -5
  5. package/src/generate-test-doc-assertions.mjs +68 -18
  6. package/src/generate-test-doc-assertions.test.mjs +22 -0
  7. package/src/generate-test-file-assertions.mjs +43 -76
  8. package/src/generate-test-output-assertions.mjs +95 -0
  9. package/src/generate-test-package-assertions.mjs +6 -0
  10. package/src/generate-test-performance-boundary-assertions.mjs +45 -0
  11. package/src/generate-test-product-fixture.mjs +1 -0
  12. package/src/generate-test-product-fixture.test.mjs +18 -0
  13. package/src/generate-test-product-iteration.mjs +1 -0
  14. package/src/generate-test-product-iteration.test.mjs +1 -0
  15. package/src/generate-test-standalone-delivery-stages.mjs +14 -6
  16. package/src/generate.test.mjs +14 -0
  17. package/templates/runtime/contracts/component-contracts.choices.test.ts +12 -9
  18. package/templates/runtime/contracts/component-contracts.choices.ts +10 -9
  19. package/templates/runtime/contracts/component-contracts.runtime-acceptance.test.ts +17 -2
  20. package/templates/runtime/contracts/component-contracts.runtime-panels.test.ts +1 -1
  21. package/templates/runtime/contracts/component-contracts.runtime-shell.test.ts +1 -1
  22. package/templates/runtime/contracts/component-contracts.runtime.ts +3 -2
  23. package/templates/runtime/contracts/decision-contracts.test.ts +25 -4
  24. package/templates/runtime/contracts/decision-contracts.ts +3 -3
  25. package/templates/runtime/contracts/performance-verification-policy.test.ts +59 -6
  26. package/templates/runtime/contracts/performance-verification-policy.ts +5 -2
  27. package/templates/runtime/export/artifact-download.test.ts +71 -0
  28. package/templates/runtime/export/artifact-download.ts +80 -0
  29. package/templates/runtime/export/artifact-export-request.ts +23 -0
  30. package/templates/runtime/export/artifact-export-settings.test.ts +75 -0
  31. package/templates/runtime/export/artifact-export-settings.ts +96 -0
  32. package/templates/runtime/export/artifact-frame-renderer.test.ts +107 -0
  33. package/templates/runtime/export/artifact-frame-renderer.ts +96 -0
  34. package/templates/runtime/export/artifact-frame-state.test.ts +54 -0
  35. package/templates/runtime/export/artifact-frame-state.ts +44 -0
  36. package/templates/runtime/export/artifact-scene-frame.test.ts +84 -0
  37. package/templates/runtime/export/artifact-scene-frame.ts +118 -0
  38. package/templates/runtime/export/export-background.ts +65 -0
  39. package/templates/runtime/export/export-error.ts +43 -0
  40. package/templates/runtime/export/export.test.ts +6 -186
  41. package/templates/runtime/export/image-artifact-export.test.ts +111 -0
  42. package/templates/runtime/export/image-artifact-export.ts +145 -0
  43. package/templates/runtime/export/index.ts +14 -1
  44. package/templates/runtime/export/product-export-renderer.test.ts +77 -0
  45. package/templates/runtime/export/product-export-renderer.ts +63 -0
  46. package/templates/runtime/export/video-artifact-export.test.ts +166 -0
  47. package/templates/runtime/export/video-artifact-export.ts +164 -0
  48. package/templates/runtime/export/video-encoding-backend.ts +124 -0
  49. package/templates/runtime/export/video-encoding-policy.test.ts +68 -0
  50. package/templates/runtime/export/video-encoding-policy.ts +86 -0
  51. package/templates/runtime/export/video-frame-schedule.test.ts +58 -0
  52. package/templates/runtime/export/video-frame-schedule.ts +42 -0
  53. package/templates/runtime/index.ts +1 -0
  54. package/templates/runtime/react/app-shell/runtime-public-api-runtime.test.tsx +291 -0
  55. package/templates/runtime/react/app-shell/runtime-public-api-test-support.ts +21 -0
  56. package/templates/runtime/react/app-shell/runtime-public-api.test.tsx +4 -303
  57. package/templates/runtime/react/app-shell/toolcraft-app.scene-export.test.tsx +12 -27
  58. package/templates/runtime/react/app-shell/toolcraft-app.tsx +22 -4
  59. package/templates/runtime/react/app-shell/toolcraft-source-asset-context.test.tsx +16 -57
  60. package/templates/runtime/react/app-shell/toolcraft-source-asset-context.tsx +75 -434
  61. package/templates/runtime/react/app-shell/toolcraft-source-asset-owner.test.ts +194 -0
  62. package/templates/runtime/react/app-shell/toolcraft-source-asset-owner.ts +227 -0
  63. package/templates/runtime/react/app-shell/toolcraft-source-asset-provider-ownership.test.tsx +243 -0
  64. package/templates/runtime/react/app-shell/toolcraft-source-asset-test-support.ts +51 -0
  65. package/templates/runtime/react/app-shell/toolcraft-unavailable-resource-proof.test.ts +138 -0
  66. package/templates/runtime/react/app-shell/toolcraft-unavailable-resource-proof.ts +378 -0
  67. package/templates/runtime/react/controls-panel/__tests__/controls-panel.footer-actions.test.tsx +7 -12
  68. package/templates/runtime/react/controls-panel/__tests__/controls-panel.scene-export-actions.test.tsx +17 -83
  69. package/templates/runtime/react/controls-panel/actions/controls-panel-actions.ts +42 -84
  70. package/templates/runtime/react/controls-panel/actions/export-action-runner.ts +62 -0
  71. package/templates/runtime/react/panel-host/panel-host-runtime-placement.test.tsx +30 -0
  72. package/templates/runtime/react/panel-host/panel-host.tsx +4 -0
  73. package/templates/runtime/scene/scene-bounds.ts +1 -2
  74. package/templates/runtime/schema/canvas-render-scale.ts +62 -0
  75. package/templates/runtime/schema/controls-panel-section-id.test.ts +1 -1
  76. package/templates/runtime/schema/controls-panel-section-roundtrip.test.ts +3 -3
  77. package/templates/runtime/schema/define-toolcraft.setup-background.test.ts +1 -1
  78. package/templates/runtime/schema/define-toolcraft.setup-render-scale-timeline.test.ts +71 -3
  79. package/templates/runtime/schema/define-toolcraft.ts +1 -1
  80. package/templates/runtime/schema/schema-resolvers.ts +0 -56
  81. package/templates/runtime/schema/types.ts +4 -4
  82. package/templates/runtime/state/canvas-background-state.ts +6 -0
  83. package/templates/runtime/testing/performance-render-plan-integration.test.ts +64 -0
  84. package/templates/runtime/testing/performance-renderer-pipeline-validation.ts +18 -1
  85. package/templates/starter/AGENTS.md +8 -7
  86. package/templates/starter/NOTICE.md +6 -0
  87. package/templates/starter/docs/toolcraft/README.md +2 -1
  88. package/templates/starter/docs/toolcraft/acceptance-testing.md +8 -6
  89. package/templates/starter/docs/toolcraft/agent-worklog.md +3 -3
  90. package/templates/starter/docs/toolcraft/assembly-workflow.md +4 -3
  91. package/templates/starter/docs/toolcraft/core/media-upload.md +2 -2
  92. package/templates/starter/docs/toolcraft/core/performance.md +14 -3
  93. package/templates/starter/docs/toolcraft/core/runtime-boundary.md +3 -1
  94. package/templates/starter/docs/toolcraft/core/setup-export.md +16 -12
  95. package/templates/starter/docs/toolcraft/core/timeline-animation.md +4 -4
  96. package/templates/starter/docs/toolcraft/performance.md +20 -6
  97. package/templates/starter/docs/toolcraft/renderer-technique.md +1 -1
  98. package/templates/starter/docs/toolcraft/schema-reference.md +2 -6
  99. package/templates/starter/docs/toolcraft/workflow.md +7 -2
  100. package/templates/starter/e2e/app-browser-control-coverage.spec.ts +33 -0
  101. package/templates/starter/e2e/app-browser-infinity-canvas-evidence.spec.ts +43 -1
  102. package/templates/starter/e2e/app-browser-render-scale-evidence.spec.ts +60 -6
  103. package/templates/starter/e2e/app-browser-render-scale-requirements.spec.ts +77 -1
  104. package/templates/starter/e2e/app-browser-runtime-provenance.spec.ts +8 -0
  105. package/templates/starter/e2e/app-browser-runtime-reporter.spec.ts +35 -0
  106. package/templates/starter/e2e/app-browser-runtime-requirements.spec.ts +4 -1
  107. package/templates/starter/e2e/app-browser-semantic-evidence.spec.ts +0 -1
  108. package/templates/starter/e2e/app-browser-shell.spec.ts +13 -5
  109. package/templates/starter/e2e/app-performance-path-adapters.ts +8 -1
  110. package/templates/starter/e2e/app-performance.spec.ts +16 -9
  111. package/templates/starter/e2e/browser-infinity-canvas-evidence.ts +9 -330
  112. package/templates/starter/e2e/browser-infinity-canvas-unavailable-image-evidence.ts +370 -0
  113. package/templates/starter/e2e/browser-media-export-evidence.spec.ts +77 -0
  114. package/templates/starter/e2e/browser-media-export-evidence.ts +208 -0
  115. package/templates/starter/e2e/browser-render-scale-evidence.ts +37 -29
  116. package/templates/starter/e2e/browser-runtime-evidence-reporter.ts +4 -1
  117. package/templates/starter/e2e/browser-runtime-evidence-requirements.ts +43 -9
  118. package/templates/starter/e2e/browser-standard-timeline-evidence.ts +106 -0
  119. package/templates/starter/e2e/decoded-pixel-observation.spec.ts +74 -0
  120. package/templates/starter/e2e/decoded-pixel-observation.ts +227 -0
  121. package/templates/starter/e2e/export-artifact-helpers.ts +206 -25
  122. package/templates/starter/e2e/image-artifact-inspection.spec.ts +41 -0
  123. package/templates/starter/e2e/image-artifact-inspection.ts +99 -0
  124. package/templates/starter/e2e/performance-canvas-helpers.ts +22 -21
  125. package/templates/starter/e2e/performance-canvas-quality-guard.spec.ts +312 -0
  126. package/templates/starter/e2e/performance-canvas-quality-guard.ts +196 -0
  127. package/templates/starter/e2e/performance-canvas-quality-observer.ts +351 -0
  128. package/templates/starter/e2e/performance-control-layout-helpers.ts +2 -2
  129. package/templates/starter/e2e/performance-path-adapter-contract.ts +4 -4
  130. package/templates/starter/e2e/performance-path-helpers.spec.ts +203 -70
  131. package/templates/starter/e2e/performance-path-helpers.ts +210 -91
  132. package/templates/starter/e2e/performance-pipeline-evidence-continuity.spec.ts +76 -0
  133. package/templates/starter/e2e/performance-pipeline-evidence-test-fixtures.ts +11 -2
  134. package/templates/starter/e2e/performance-pipeline-invariants.ts +47 -4
  135. package/templates/starter/e2e/performance-render-scale-runner.spec.ts +140 -38
  136. package/templates/starter/e2e/video-artifact-inspection.spec.ts +64 -0
  137. package/templates/starter/e2e/video-artifact-inspection.ts +281 -0
  138. package/templates/starter/gitignore +1 -0
  139. package/templates/starter/package.json +1 -0
  140. package/templates/starter/scripts/check-toolcraft-docs.mjs +17 -0
  141. package/templates/starter/scripts/toolcraft-delivery-plan-authority.test.mjs +60 -0
  142. package/templates/starter/scripts/toolcraft-delivery-plan-errors.test.mjs +7 -2
  143. package/templates/starter/scripts/toolcraft-delivery-plan.mjs +7 -3
  144. package/templates/starter/scripts/toolcraft-product-boundary-ast.mjs +24 -2
  145. package/templates/starter/scripts/toolcraft-product-boundary.mjs +12 -0
  146. package/templates/starter/scripts/toolcraft-product-boundary.test.mjs +46 -0
  147. package/templates/starter/scripts/toolcraft-product-evidence-boundary.test.mjs +85 -20
  148. package/templates/starter/scripts/toolcraft-product-evidence-import-policy.mjs +43 -0
  149. package/templates/starter/scripts/toolcraft-product-export-boundary.mjs +168 -0
  150. package/templates/starter/scripts/toolcraft-typescript-analysis.d.mts +73 -0
  151. package/templates/starter/scripts/toolcraft-unavailable-resource-evidence-facade-test-helpers.mjs +318 -0
  152. package/templates/starter/scripts/toolcraft-unavailable-resource-evidence-facade.test.mjs +155 -206
  153. package/templates/starter/scripts/toolcraft-verification-inventory.mjs +1 -0
  154. package/templates/starter/scripts/toolcraft-verification-receipt.test.mjs +51 -0
  155. package/templates/starter/scripts/toolcraft-vite-env-activation.test.mjs +128 -59
  156. package/templates/starter/src/app/acceptance/export-artifact-coverage.ts +113 -0
  157. package/templates/starter/src/app/acceptance/output-background-rules.ts +1 -1
  158. package/templates/starter/src/app/acceptance/output-export.ts +13 -2
  159. package/templates/starter/src/app/acceptance/types.ts +7 -0
  160. package/templates/starter/src/app/acceptance/validate-coverage.ts +2 -1
  161. package/templates/starter/src/app/starter-acceptance.export-artifact.test.ts +131 -0
  162. package/templates/starter/src/app/starter-acceptance.keyframes.test.ts +14 -4
  163. package/templates/starter/src/app/starter-acceptance.output-export-rules.test.ts +42 -1
  164. package/templates/starter/src/app/starter-acceptance.ts +2 -0
  165. package/templates/starter/src/app/starter-performance-test-utils.ts +103 -0
  166. package/templates/starter/src/app/starter-performance.gates.test.ts +231 -278
  167. package/templates/starter/src/app/starter-performance.lifecycle.test.ts +261 -0
  168. package/templates/starter/src/app/test-evidence/browser-runtime-contract.ts +2 -0
  169. package/templates/starter/src/routes/index.tsx +1 -0
  170. package/templates/runtime/export/export.ts +0 -166
@@ -0,0 +1,51 @@
1
+ import { vi } from "vitest";
2
+
3
+ import { defineToolcraft } from "../../schema/define-toolcraft";
4
+ import type { ToolcraftSourceAssetCoordinator } from "../../source-assets/source-asset-coordinator";
5
+ import { createToolcraftState } from "../../state/create-template-state";
6
+ import { createToolcraftExternalStore } from "../../state/toolcraft-external-store";
7
+ import { useToolcraftSourceAssetCoordinator } from "./toolcraft-source-asset-context";
8
+
9
+ export function ToolcraftSourceAssetCoordinatorProbe({
10
+ capture,
11
+ }: {
12
+ capture: (coordinator: ToolcraftSourceAssetCoordinator) => void;
13
+ }): null {
14
+ capture(useToolcraftSourceAssetCoordinator());
15
+ return null;
16
+ }
17
+
18
+ export function createTestSourceAssetCoordinator(
19
+ overrides: Partial<ToolcraftSourceAssetCoordinator> = {},
20
+ ): ToolcraftSourceAssetCoordinator {
21
+ const idleOperation = Object.freeze({
22
+ phase: "idle" as const,
23
+ target: "source.test",
24
+ });
25
+
26
+ return {
27
+ cancelTarget: vi.fn(),
28
+ clearPresentationFeedback: vi.fn(),
29
+ dispose: vi.fn(async () => undefined),
30
+ getOperation: () => idleOperation,
31
+ getPresentation: () => null,
32
+ hydrateBinaryMedia: vi.fn(async () => undefined),
33
+ hydrateModels: vi.fn(async () => undefined),
34
+ importBatch: vi.fn(async () => ({
35
+ assetIds: [] as const,
36
+ kind: "committed" as const,
37
+ })),
38
+ repairModel: vi.fn(async () => ({ kind: "cancelled" as const })),
39
+ reportPresentationFeedback: vi.fn(),
40
+ subscribe: () => () => undefined,
41
+ ...overrides,
42
+ };
43
+ }
44
+
45
+ export function createTestSourceAssetStore() {
46
+ return createToolcraftExternalStore(
47
+ createToolcraftState(
48
+ defineToolcraft({ canvas: { enabled: true, upload: true }, panels: {} }),
49
+ ),
50
+ );
51
+ }
@@ -0,0 +1,138 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+
3
+ import { defineToolcraft } from "../../schema/define-toolcraft";
4
+ import { createToolcraftState } from "../../state/create-template-state";
5
+ import {
6
+ createToolcraftExternalStore,
7
+ type ToolcraftExternalStore,
8
+ } from "../../state/toolcraft-external-store";
9
+ import type { ToolcraftCommand } from "../../state/types";
10
+ import { createTestSourceAssetCoordinator } from "./toolcraft-source-asset-test-support";
11
+ import { decorateToolcraftUnavailableResourceProofCoordinator } from "./toolcraft-unavailable-resource-proof";
12
+
13
+ const PROOF_REQUEST = "toolcraft.browser-proof.unavailable-resource-request";
14
+ const RESOURCE_REF = `media:image:sha256:${"d".repeat(64)}`;
15
+
16
+ function createDeferred<T>() {
17
+ let resolve!: (value: T) => void;
18
+ const promise = new Promise<T>((resolvePromise) => {
19
+ resolve = resolvePromise;
20
+ });
21
+ return { promise, resolve };
22
+ }
23
+
24
+ function createReadyImageStore(): ToolcraftExternalStore {
25
+ return createToolcraftExternalStore(
26
+ createToolcraftState(
27
+ defineToolcraft({ canvas: { enabled: true }, panels: {} }),
28
+ {
29
+ mediaAssets: [{
30
+ assetKind: "image",
31
+ fileName: "fixture.png",
32
+ id: "fixture-image",
33
+ layerId: "fixture-layer",
34
+ lifecycle: "ready",
35
+ mimeType: "image/png",
36
+ position: { x: 0, y: 0 },
37
+ resourceRef: RESOURCE_REF,
38
+ size: { height: 20, unit: "px", width: 20 },
39
+ sourceTarget: "source.images",
40
+ }],
41
+ },
42
+ ),
43
+ );
44
+ }
45
+
46
+ function dispatchProofRequest(action: "activate" | "cleanup"): void {
47
+ document.dispatchEvent(new CustomEvent(PROOF_REQUEST, {
48
+ detail: {
49
+ action,
50
+ fileName: "fixture.png",
51
+ requestId: "proof-request",
52
+ sourceTarget: "source.images",
53
+ },
54
+ }));
55
+ }
56
+
57
+ describe("Toolcraft unavailable-resource proof coordinator", () => {
58
+ it("settles an aborted pending request without a post-dispose transition", async () => {
59
+ const deferredBytes = createDeferred<Uint8Array>();
60
+ const baseDispose = vi.fn(async () => undefined);
61
+ const resolveResource = vi.fn(
62
+ (
63
+ _ref: string,
64
+ _options: Readonly<{ signal: AbortSignal }>,
65
+ ) => deferredBytes.promise,
66
+ );
67
+ const backingStore = createReadyImageStore();
68
+ const dispatch = vi.fn(backingStore.dispatch);
69
+ const store: ToolcraftExternalStore = { ...backingStore, dispatch };
70
+ const coordinator = decorateToolcraftUnavailableResourceProofCoordinator(
71
+ createTestSourceAssetCoordinator({
72
+ dispose: baseDispose,
73
+ resolveResource,
74
+ }),
75
+ store,
76
+ );
77
+
78
+ dispatchProofRequest("activate");
79
+ await vi.waitFor(() => expect(resolveResource).toHaveBeenCalledTimes(1));
80
+
81
+ const disposal = coordinator.dispose();
82
+ deferredBytes.resolve(new Uint8Array([1, 2, 3]));
83
+ await disposal;
84
+ await Promise.resolve();
85
+ await Promise.resolve();
86
+ await Promise.resolve();
87
+
88
+ expect(store.getCommittedState().mediaAssets[0]).toMatchObject({
89
+ lifecycle: "ready",
90
+ });
91
+ expect(resolveResource.mock.calls[0]?.[1].signal.aborted).toBe(true);
92
+ expect(dispatch).not.toHaveBeenCalled();
93
+ expect(baseDispose).toHaveBeenCalledTimes(1);
94
+ });
95
+
96
+ it("attempts base disposal when proof restoration dispatch fails", async () => {
97
+ const dispatchFailure = new Error("proof restoration dispatch failed");
98
+ const baseFailure = new Error("base coordinator disposal failed");
99
+ const baseDispose = vi.fn(async () => Promise.reject(baseFailure));
100
+ const backingStore = createReadyImageStore();
101
+ const store: ToolcraftExternalStore = {
102
+ ...backingStore,
103
+ dispatch: vi.fn((command: ToolcraftCommand) => {
104
+ if (
105
+ command.type === "media.setBinaryResourceState" &&
106
+ command.lifecycle === "ready"
107
+ ) {
108
+ throw dispatchFailure;
109
+ }
110
+ backingStore.dispatch(command);
111
+ }),
112
+ };
113
+ const coordinator = decorateToolcraftUnavailableResourceProofCoordinator(
114
+ createTestSourceAssetCoordinator({
115
+ dispose: baseDispose,
116
+ resolveResource: vi.fn(async () => new Uint8Array([1, 2, 3])),
117
+ }),
118
+ store,
119
+ );
120
+
121
+ dispatchProofRequest("activate");
122
+ await vi.waitFor(() =>
123
+ expect(store.getCommittedState().mediaAssets[0]).toMatchObject({
124
+ lifecycle: "unavailable",
125
+ })
126
+ );
127
+
128
+ const disposal = coordinator.dispose();
129
+ const repeatedDisposal = coordinator.dispose();
130
+
131
+ await expect(disposal).rejects.toMatchObject({
132
+ errors: [dispatchFailure, baseFailure],
133
+ });
134
+ expect(repeatedDisposal).toBe(disposal);
135
+ await expect(repeatedDisposal).rejects.toBeInstanceOf(AggregateError);
136
+ expect(baseDispose).toHaveBeenCalledTimes(1);
137
+ });
138
+ });
@@ -0,0 +1,378 @@
1
+ /// <reference types="vite/client" />
2
+
3
+ import type { ToolcraftSourceAssetCoordinator } from "../../source-assets/source-asset-coordinator";
4
+ import type { ToolcraftExternalStore } from "../../state/toolcraft-external-store";
5
+ import type { ToolcraftImageAsset } from "../../state/types";
6
+
7
+ const TOOLCRAFT_UNAVAILABLE_RESOURCE_PROOF_REQUEST =
8
+ "toolcraft.browser-proof.unavailable-resource-request";
9
+ const TOOLCRAFT_UNAVAILABLE_RESOURCE_PROOF_ATTRIBUTE =
10
+ "data-toolcraft-unavailable-resource-evidence";
11
+ const toolcraftUnavailableResourceProofEnabled =
12
+ import.meta.env?.MODE === "test" ||
13
+ import.meta.env?.VITE_TOOLCRAFT_BROWSER_PROOF_FIXTURES ===
14
+ "unavailable-resource";
15
+
16
+ type ToolcraftUnavailableResourceProofRequest = Readonly<{
17
+ action: "activate" | "cleanup";
18
+ fileName: string;
19
+ requestId: string;
20
+ sourceTarget?: string;
21
+ }>;
22
+
23
+ type ActiveUnavailableResourceProof = Readonly<{
24
+ assetId: string;
25
+ requestId: string;
26
+ resourceRef: string;
27
+ }>;
28
+
29
+ type PendingUnavailableResourceProofRequest = Readonly<{
30
+ abortController: AbortController;
31
+ promise: Promise<void>;
32
+ requestId: string;
33
+ }>;
34
+
35
+ const cancelledProofRequest = new Error(
36
+ "Unavailable-resource proof request was cancelled.",
37
+ );
38
+
39
+ function readUnavailableResourceProofRequest(
40
+ event: Event,
41
+ ): ToolcraftUnavailableResourceProofRequest | null {
42
+ if (!(event instanceof CustomEvent)) return null;
43
+ const detail: unknown = event.detail;
44
+ if (typeof detail !== "object" || detail === null || Array.isArray(detail)) {
45
+ return null;
46
+ }
47
+
48
+ const request = detail as Partial<ToolcraftUnavailableResourceProofRequest>;
49
+ if (
50
+ (request.action !== "activate" && request.action !== "cleanup") ||
51
+ typeof request.fileName !== "string" ||
52
+ request.fileName.trim() !== request.fileName ||
53
+ request.fileName.length === 0 ||
54
+ typeof request.requestId !== "string" ||
55
+ request.requestId.trim() !== request.requestId ||
56
+ request.requestId.length === 0 ||
57
+ (
58
+ request.sourceTarget !== undefined &&
59
+ (
60
+ typeof request.sourceTarget !== "string" ||
61
+ request.sourceTarget.trim() !== request.sourceTarget ||
62
+ request.sourceTarget.length === 0
63
+ )
64
+ )
65
+ ) {
66
+ return null;
67
+ }
68
+
69
+ return request as ToolcraftUnavailableResourceProofRequest;
70
+ }
71
+
72
+ export function decorateToolcraftUnavailableResourceProofCoordinator(
73
+ coordinator: ToolcraftSourceAssetCoordinator,
74
+ store: ToolcraftExternalStore,
75
+ ): ToolcraftSourceAssetCoordinator {
76
+ if (!toolcraftUnavailableResourceProofEnabled) return coordinator;
77
+
78
+ const resolveResource = coordinator.resolveResource;
79
+ if (!resolveResource) return coordinator;
80
+ const disposeCoordinator = coordinator.dispose;
81
+
82
+ const unavailableResourceRefs = new Set<string>();
83
+ let active: ActiveUnavailableResourceProof | null = null;
84
+ let disposed = false;
85
+ let disposePromise: Promise<void> | null = null;
86
+ let pendingRequest: PendingUnavailableResourceProofRequest | null = null;
87
+
88
+ const proofResolveResource = async (
89
+ ref: string,
90
+ options: Readonly<{ signal: AbortSignal }>,
91
+ ): Promise<Uint8Array | null> =>
92
+ unavailableResourceRefs.has(ref)
93
+ ? null
94
+ : resolveResource(ref, options);
95
+
96
+ const publish = (
97
+ requestId: string,
98
+ observation: Readonly<Record<string, boolean | string>>,
99
+ ): void => {
100
+ const root = document.querySelector<HTMLElement>(
101
+ '[data-slot="toolcraft-runtime-app"]',
102
+ );
103
+ if (!root) return;
104
+ root.setAttribute(
105
+ TOOLCRAFT_UNAVAILABLE_RESOURCE_PROOF_ATTRIBUTE,
106
+ JSON.stringify({ requestId, ...observation }),
107
+ );
108
+ };
109
+
110
+ const handleRequest = (event: Event): void => {
111
+ const request = readUnavailableResourceProofRequest(event);
112
+ if (!request || disposed) return;
113
+
114
+ if (pendingRequest) {
115
+ publish(request.requestId, {
116
+ message: `Unavailable-resource proof request "${pendingRequest.requestId}" is still pending.`,
117
+ status: "error",
118
+ });
119
+ return;
120
+ }
121
+ const abortController = new AbortController();
122
+ const assertRequestActive = (): void => {
123
+ if (disposed || abortController.signal.aborted) {
124
+ throw cancelledProofRequest;
125
+ }
126
+ };
127
+ const promise = Promise.resolve().then(async () => {
128
+ assertRequestActive();
129
+ if (request.action === "cleanup") {
130
+ if (!active || active.requestId !== request.requestId) {
131
+ throw new Error(
132
+ "Unavailable-resource proof cleanup must match the active fixture request.",
133
+ );
134
+ }
135
+
136
+ const current = active;
137
+ unavailableResourceRefs.delete(current.resourceRef);
138
+ const restoredBytes = await proofResolveResource(
139
+ current.resourceRef,
140
+ { signal: abortController.signal },
141
+ );
142
+ assertRequestActive();
143
+ if (!restoredBytes?.byteLength) {
144
+ throw new Error(
145
+ "Unavailable-resource proof cleanup could not restore the real resolver.",
146
+ );
147
+ }
148
+ store.dispatch({
149
+ assetId: current.assetId,
150
+ expectedResourceRef: current.resourceRef,
151
+ lifecycle: "ready",
152
+ type: "media.setBinaryResourceState",
153
+ });
154
+ assertRequestActive();
155
+ const restored = store
156
+ .getCommittedState()
157
+ .mediaAssets.find((asset) => asset.id === current.assetId);
158
+ if (
159
+ !restored ||
160
+ restored.assetKind !== "image" ||
161
+ restored.lifecycle !== "ready"
162
+ ) {
163
+ throw new Error(
164
+ "Unavailable-resource proof cleanup did not restore the typed resource state.",
165
+ );
166
+ }
167
+
168
+ active = null;
169
+ assertRequestActive();
170
+ publish(request.requestId, {
171
+ assetId: current.assetId,
172
+ fileName: request.fileName,
173
+ lifecycle: "ready",
174
+ resolverAvailable: true,
175
+ status: "cleaned",
176
+ });
177
+ return;
178
+ }
179
+
180
+ if (active) {
181
+ throw new Error(
182
+ "Unavailable-resource proof supports exactly one active resource fixture.",
183
+ );
184
+ }
185
+ const candidates = store
186
+ .getCommittedState()
187
+ .mediaAssets.filter(
188
+ (
189
+ asset,
190
+ ): asset is ToolcraftImageAsset & { lifecycle: "ready" } =>
191
+ asset.assetKind === "image" &&
192
+ asset.lifecycle === "ready" &&
193
+ asset.fileName === request.fileName &&
194
+ (
195
+ request.sourceTarget === undefined ||
196
+ asset.sourceTarget === request.sourceTarget
197
+ ),
198
+ );
199
+ if (candidates.length !== 1) {
200
+ throw new Error(
201
+ `Unavailable-resource proof requires exactly one ready image named "${request.fileName}", received ${candidates.length}.`,
202
+ );
203
+ }
204
+ const asset = candidates[0];
205
+ if (!asset) {
206
+ throw new Error("Unavailable-resource proof image selection failed.");
207
+ }
208
+
209
+ const initialBytes = await proofResolveResource(
210
+ asset.resourceRef,
211
+ { signal: abortController.signal },
212
+ );
213
+ assertRequestActive();
214
+ if (!initialBytes?.byteLength) {
215
+ throw new Error(
216
+ "Unavailable-resource proof requires a resource available through the real resolver before fault activation.",
217
+ );
218
+ }
219
+
220
+ unavailableResourceRefs.add(asset.resourceRef);
221
+ const activatedProof: ActiveUnavailableResourceProof = {
222
+ assetId: asset.id,
223
+ requestId: request.requestId,
224
+ resourceRef: asset.resourceRef,
225
+ };
226
+ try {
227
+ const faultedBytes = await proofResolveResource(
228
+ asset.resourceRef,
229
+ { signal: abortController.signal },
230
+ );
231
+ assertRequestActive();
232
+ if (faultedBytes !== null) {
233
+ throw new Error(
234
+ "Unavailable-resource proof did not isolate the requested resolver resource.",
235
+ );
236
+ }
237
+ active = activatedProof;
238
+ store.dispatch({
239
+ assetId: asset.id,
240
+ error: {
241
+ code: "browser-proof-resource-unavailable",
242
+ message:
243
+ "The resource is intentionally unavailable for protected browser proof.",
244
+ },
245
+ expectedResourceRef: asset.resourceRef,
246
+ lifecycle: "unavailable",
247
+ type: "media.setBinaryResourceState",
248
+ });
249
+ assertRequestActive();
250
+ const unavailable = store
251
+ .getCommittedState()
252
+ .mediaAssets.find((candidate) => candidate.id === asset.id);
253
+ if (
254
+ !unavailable ||
255
+ unavailable.assetKind !== "image" ||
256
+ unavailable.lifecycle !== "unavailable"
257
+ ) {
258
+ throw new Error(
259
+ "Unavailable-resource proof did not reach the typed unavailable state.",
260
+ );
261
+ }
262
+
263
+ assertRequestActive();
264
+ publish(request.requestId, {
265
+ assetId: asset.id,
266
+ fileName: asset.fileName,
267
+ lifecycle: "unavailable",
268
+ resolverAvailable: false,
269
+ status: "active",
270
+ });
271
+ } catch (error) {
272
+ unavailableResourceRefs.delete(asset.resourceRef);
273
+ if (!disposed && active === activatedProof) active = null;
274
+ throw error;
275
+ }
276
+ });
277
+ const trackedRequest: PendingUnavailableResourceProofRequest = {
278
+ abortController,
279
+ promise,
280
+ requestId: request.requestId,
281
+ };
282
+ pendingRequest = trackedRequest;
283
+ void promise
284
+ .catch((error: unknown) => {
285
+ if (
286
+ error === cancelledProofRequest ||
287
+ disposed ||
288
+ abortController.signal.aborted
289
+ ) {
290
+ return;
291
+ }
292
+ publish(request.requestId, {
293
+ message:
294
+ error instanceof Error
295
+ ? error.message
296
+ : "Unavailable-resource proof fixture failed.",
297
+ status: "error",
298
+ });
299
+ })
300
+ .finally(() => {
301
+ if (pendingRequest === trackedRequest) pendingRequest = null;
302
+ });
303
+ };
304
+
305
+ const dispose = (): Promise<void> => {
306
+ if (!disposePromise) {
307
+ disposed = true;
308
+ document.removeEventListener(
309
+ TOOLCRAFT_UNAVAILABLE_RESOURCE_PROOF_REQUEST,
310
+ handleRequest,
311
+ );
312
+ const requestAtDisposal = pendingRequest;
313
+ disposePromise = Promise.resolve().then(async () => {
314
+ const failures: unknown[] = [];
315
+ requestAtDisposal?.abortController.abort();
316
+ if (requestAtDisposal) {
317
+ try {
318
+ await requestAtDisposal.promise;
319
+ } catch {
320
+ // Request failures are fixture observations, not disposal failures.
321
+ }
322
+ if (pendingRequest === requestAtDisposal) pendingRequest = null;
323
+ }
324
+ const activeAtDisposal = active;
325
+ if (activeAtDisposal) {
326
+ unavailableResourceRefs.delete(activeAtDisposal.resourceRef);
327
+ try {
328
+ store.dispatch({
329
+ assetId: activeAtDisposal.assetId,
330
+ expectedResourceRef: activeAtDisposal.resourceRef,
331
+ lifecycle: "ready",
332
+ type: "media.setBinaryResourceState",
333
+ });
334
+ const restored = store
335
+ .getCommittedState()
336
+ .mediaAssets.find(
337
+ (asset) => asset.id === activeAtDisposal.assetId,
338
+ );
339
+ if (
340
+ !restored ||
341
+ restored.assetKind !== "image" ||
342
+ restored.lifecycle !== "ready"
343
+ ) {
344
+ throw new Error(
345
+ "Unavailable-resource proof disposal did not restore the typed resource state.",
346
+ );
347
+ }
348
+ } catch (error) {
349
+ failures.push(error);
350
+ } finally {
351
+ active = null;
352
+ }
353
+ }
354
+ unavailableResourceRefs.clear();
355
+ try {
356
+ await disposeCoordinator.call(coordinator);
357
+ } catch (error) {
358
+ failures.push(error);
359
+ }
360
+ if (failures.length > 0) {
361
+ throw new AggregateError(
362
+ failures,
363
+ "Toolcraft unavailable-resource proof failed to dispose.",
364
+ );
365
+ }
366
+ });
367
+ }
368
+ return disposePromise;
369
+ };
370
+
371
+ coordinator.dispose = dispose;
372
+ coordinator.resolveResource = proofResolveResource;
373
+ document.addEventListener(
374
+ TOOLCRAFT_UNAVAILABLE_RESOURCE_PROOF_REQUEST,
375
+ handleRequest,
376
+ );
377
+ return coordinator;
378
+ }
@@ -66,7 +66,7 @@ describe("ControlsPanel footer actions and settings transfer", () => {
66
66
  expect(handledActions).toEqual(["download:Generation Controls"]);
67
67
  });
68
68
 
69
- it("exposes the runtime model compositor to product export actions", async () => {
69
+ it("keeps runtime export mechanics out of generic product action context", () => {
70
70
  const schema = defineToolcraft({
71
71
  canvas: { enabled: true },
72
72
  panels: {
@@ -93,24 +93,19 @@ describe("ControlsPanel footer actions and settings transfer", () => {
93
93
  },
94
94
  },
95
95
  });
96
- const canvas = {
97
- getContext: vi.fn(() => ({ drawImage: vi.fn() })),
98
- height: 1080,
99
- width: 1920,
100
- } as unknown as HTMLCanvasElement;
101
- let renderedModels: Promise<number> | undefined;
96
+ let actionContextKeys: string[] = [];
102
97
 
103
98
  renderControlsPanelWithSchema(schema, {
104
- onPanelAction: ({ renderModelsToCanvas }) => {
105
- renderedModels = renderModelsToCanvas(canvas);
106
- return renderedModels;
99
+ onPanelAction: (context) => {
100
+ actionContextKeys = Object.keys(context).sort();
107
101
  },
108
102
  });
109
103
 
110
104
  fireEvent.click(screen.getByRole("button", { name: "Export PNG" }));
111
105
 
112
- await expect(renderedModels).resolves.toBe(0);
113
- expect(canvas.getContext).toHaveBeenCalledWith("2d");
106
+ expect(actionContextKeys).not.toContain("renderModelsToCanvas");
107
+ expect(actionContextKeys).not.toContain("renderRuntimeSceneToCanvas");
108
+ expect(actionContextKeys).not.toContain("resolveSceneExportFrame");
114
109
  });
115
110
 
116
111
  it("renders runtime settings transfer as a headerless body section instead of sticky footer actions", () => {