@1agh/maude 0.42.0 → 0.44.0
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/apps/studio/.ai/cache/_stats.json +7 -0
- package/apps/studio/acp/bootstrap-brief.ts +12 -2
- package/apps/studio/acp/bridge.ts +44 -10
- package/apps/studio/acp/env.ts +23 -0
- package/apps/studio/acp/login-state.ts +263 -0
- package/apps/studio/acp/plugin-bootstrap.ts +31 -25
- package/apps/studio/acp/probe.ts +152 -1
- package/apps/studio/annotations-layer.tsx +1451 -324
- package/apps/studio/annotations-model.ts +58 -0
- package/apps/studio/api.ts +568 -24
- package/apps/studio/bin/_audio-search.mjs +140 -0
- package/apps/studio/bin/_import-asset-pdf-worker.mjs +34 -0
- package/apps/studio/bin/_import-asset.mjs +815 -0
- package/apps/studio/bin/_import-brand.mjs +635 -0
- package/apps/studio/bin/_import-tokens-alias-resolver.mjs +95 -0
- package/apps/studio/bin/_import-tokens.mjs +1201 -0
- package/apps/studio/bin/_ingest-footage.mjs +386 -0
- package/apps/studio/bin/_probe-footage-playwright.mjs +269 -0
- package/apps/studio/bin/_transcribe.mjs +419 -0
- package/apps/studio/bin/_transcribe.test.mjs +80 -0
- package/apps/studio/bin/_video-playwright.mjs +141 -71
- package/apps/studio/bin/audio-search.sh +28 -0
- package/apps/studio/bin/generate.sh +154 -0
- package/apps/studio/bin/import-asset.sh +34 -0
- package/apps/studio/bin/import-brand.sh +29 -0
- package/apps/studio/bin/import-tokens.sh +33 -0
- package/apps/studio/bin/ingest-footage.sh +27 -0
- package/apps/studio/bin/photo-adjust.sh +163 -0
- package/apps/studio/bin/photo-bg-remove.sh +258 -0
- package/apps/studio/bin/probe-footage.sh +28 -0
- package/apps/studio/bin/read-annotations.mjs +125 -2
- package/apps/studio/bin/transcribe.sh +39 -0
- package/apps/studio/canvas-edit.ts +656 -12
- package/apps/studio/canvas-lib.tsx +637 -10
- package/apps/studio/canvas-pipeline.ts +65 -7
- package/apps/studio/canvas-shell.tsx +156 -16
- package/apps/studio/client/app.jsx +2385 -699
- package/apps/studio/client/export-center.jsx +35 -1
- package/apps/studio/client/generate-dialog.jsx +352 -0
- package/apps/studio/client/github.js +18 -0
- package/apps/studio/client/inspector-controls.jsx +781 -0
- package/apps/studio/client/panels/BrandUploadPanel.jsx +233 -0
- package/apps/studio/client/panels/ChatPanel.jsx +15 -33
- package/apps/studio/client/panels/GitPanel.jsx +8 -0
- package/apps/studio/client/panels/IntroVideoDialog.jsx +44 -0
- package/apps/studio/client/panels/OnboardingWizard.jsx +12 -0
- package/apps/studio/client/panels/ReadinessList.jsx +227 -7
- package/apps/studio/client/panels/RepoBranchSwitcher.jsx +133 -21
- package/apps/studio/client/panels/SettingsPanel.jsx +828 -0
- package/apps/studio/client/panels/SetupChecklist.jsx +223 -0
- package/apps/studio/client/panels/StickerPicker.jsx +160 -0
- package/apps/studio/client/panels/TimelinePanel.jsx +15 -2
- package/apps/studio/client/photo-knobs.jsx +432 -0
- package/apps/studio/client/styles/3-shell-maude.css +376 -28
- package/apps/studio/client/styles/4-components.css +223 -0
- package/apps/studio/client/styles/6-acp-chat.css +86 -0
- package/apps/studio/client/tour/quick-setup-tour.js +39 -0
- package/apps/studio/commands/annotation-strokes-command.ts +13 -3
- package/apps/studio/commands/edit-source-command.ts +31 -4
- package/apps/studio/config.schema.json +70 -0
- package/apps/studio/context-menu.tsx +42 -8
- package/apps/studio/context.ts +16 -0
- package/apps/studio/design-setup-readiness.ts +115 -0
- package/apps/studio/dist/client.bundle.js +5572 -21
- package/apps/studio/dist/comment-mount.js +2 -2
- package/apps/studio/dist/runtime/.min-sizes.json +1 -0
- package/apps/studio/dist/runtime/@imgly_background-removal.js +5543 -0
- package/apps/studio/dist/runtime/pixi-js.js +519 -519
- package/apps/studio/dist/styles.css +1 -1
- package/apps/studio/dom-selection.ts +25 -0
- package/apps/studio/draw/palette.ts +34 -0
- package/apps/studio/exporters/jobs.ts +47 -5
- package/apps/studio/exporters/video.ts +24 -4
- package/apps/studio/footage/schema.test.ts +287 -0
- package/apps/studio/footage/schema.ts +662 -0
- package/apps/studio/footage-store.ts +235 -0
- package/apps/studio/fs-watch.ts +10 -0
- package/apps/studio/generation/adapters/elevenlabs.ts +370 -0
- package/apps/studio/generation/adapters/gemini.ts +542 -0
- package/apps/studio/generation/adapters/groq.test.ts +80 -0
- package/apps/studio/generation/adapters/groq.ts +247 -0
- package/apps/studio/generation/audio-library.test.ts +95 -0
- package/apps/studio/generation/audio-library.ts +156 -0
- package/apps/studio/generation/captions.test.ts +78 -0
- package/apps/studio/generation/captions.ts +132 -0
- package/apps/studio/generation/download.ts +76 -0
- package/apps/studio/generation/elevenlabs.test.ts +223 -0
- package/apps/studio/generation/gemini.test.ts +350 -0
- package/apps/studio/generation/jobs.test.ts +128 -0
- package/apps/studio/generation/jobs.ts +243 -0
- package/apps/studio/generation/keys.test.ts +75 -0
- package/apps/studio/generation/keys.ts +162 -0
- package/apps/studio/generation/prefs.test.ts +95 -0
- package/apps/studio/generation/prefs.ts +82 -0
- package/apps/studio/generation/registry.ts +88 -0
- package/apps/studio/generation/types.test.ts +123 -0
- package/apps/studio/generation/types.ts +266 -0
- package/apps/studio/generation/whisper-models.test.ts +152 -0
- package/apps/studio/generation/whisper-models.ts +257 -0
- package/apps/studio/generation/whisper-spike-results.md +61 -0
- package/apps/studio/git/endpoints.ts +68 -7
- package/apps/studio/git/service.ts +199 -21
- package/apps/studio/github/service.ts +67 -0
- package/apps/studio/handoff.ts +3 -2
- package/apps/studio/http.ts +1039 -22
- package/apps/studio/input-router.tsx +10 -0
- package/apps/studio/inspect.ts +53 -1
- package/apps/studio/media/intro.mp4 +0 -0
- package/apps/studio/media-commit-chain.ts +119 -0
- package/apps/studio/paths.ts +20 -0
- package/apps/studio/photo/filters.ts +285 -0
- package/apps/studio/photo/pipeline.ts +607 -0
- package/apps/studio/photo/schema.ts +367 -0
- package/apps/studio/photo-store.ts +154 -0
- package/apps/studio/readiness.ts +108 -20
- package/apps/studio/runtime-bundle.ts +29 -6
- package/apps/studio/scaffold-design.ts +1145 -0
- package/apps/studio/server.ts +18 -2
- package/apps/studio/stickers/figjam-doodle/manifest.json +104 -0
- package/apps/studio/stickers/figjam-doodle/slice1.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice10.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice11.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice12.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice13.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice14.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice15.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice16.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice17.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice18.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice19.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice2.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice20.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice21.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice22.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice23.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice24.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice3.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice4.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice5.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice6.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice7.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice8.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice9.png +0 -0
- package/apps/studio/stickers/life-style/best.png +0 -0
- package/apps/studio/stickers/life-style/come-on.png +0 -0
- package/apps/studio/stickers/life-style/cut.png +0 -0
- package/apps/studio/stickers/life-style/detox.png +0 -0
- package/apps/studio/stickers/life-style/just.png +0 -0
- package/apps/studio/stickers/life-style/manifest.json +68 -0
- package/apps/studio/stickers/life-style/mirror.png +0 -0
- package/apps/studio/stickers/life-style/nice.png +0 -0
- package/apps/studio/stickers/life-style/objects.png +0 -0
- package/apps/studio/stickers/life-style/ok.png +0 -0
- package/apps/studio/stickers/life-style/queen.png +0 -0
- package/apps/studio/stickers/life-style/star.png +0 -0
- package/apps/studio/stickers/life-style/sun.png +0 -0
- package/apps/studio/stickers/life-style/water.png +0 -0
- package/apps/studio/stickers/life-style/yeah.png +0 -0
- package/apps/studio/stickers/life-style/you-can.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/check-the-deets.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/cut-it.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/dope.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/fresh.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/hot.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/idea.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/keep-exploring.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/killed-it.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/love-it.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/manifest.json +88 -0
- package/apps/studio/stickers/opposing-thoughts/not-sure.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/ok.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/pure-gold.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/save-for-later.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/steal-this.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/take-a-peek.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/this-or-that.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/thoughts.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/vibes.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/winner.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/wip.png +0 -0
- package/apps/studio/stickers/project-status/group-100.png +0 -0
- package/apps/studio/stickers/project-status/group-101.png +0 -0
- package/apps/studio/stickers/project-status/group-102.png +0 -0
- package/apps/studio/stickers/project-status/group-103.png +0 -0
- package/apps/studio/stickers/project-status/group-104.png +0 -0
- package/apps/studio/stickers/project-status/group-105.png +0 -0
- package/apps/studio/stickers/project-status/group-106.png +0 -0
- package/apps/studio/stickers/project-status/group-107.png +0 -0
- package/apps/studio/stickers/project-status/group-108.png +0 -0
- package/apps/studio/stickers/project-status/group-109.png +0 -0
- package/apps/studio/stickers/project-status/group-110.png +0 -0
- package/apps/studio/stickers/project-status/group-111.png +0 -0
- package/apps/studio/stickers/project-status/group-112.png +0 -0
- package/apps/studio/stickers/project-status/group-113.png +0 -0
- package/apps/studio/stickers/project-status/group-114.png +0 -0
- package/apps/studio/stickers/project-status/group-115.png +0 -0
- package/apps/studio/stickers/project-status/group-117.png +0 -0
- package/apps/studio/stickers/project-status/group-118.png +0 -0
- package/apps/studio/stickers/project-status/group-119.png +0 -0
- package/apps/studio/stickers/project-status/group-120.png +0 -0
- package/apps/studio/stickers/project-status/group-121.png +0 -0
- package/apps/studio/stickers/project-status/group-122.png +0 -0
- package/apps/studio/stickers/project-status/group-41.png +0 -0
- package/apps/studio/stickers/project-status/group-42.png +0 -0
- package/apps/studio/stickers/project-status/group-43.png +0 -0
- package/apps/studio/stickers/project-status/group-44.png +0 -0
- package/apps/studio/stickers/project-status/group-45.png +0 -0
- package/apps/studio/stickers/project-status/group-46.png +0 -0
- package/apps/studio/stickers/project-status/group-47.png +0 -0
- package/apps/studio/stickers/project-status/group-48.png +0 -0
- package/apps/studio/stickers/project-status/group-49.png +0 -0
- package/apps/studio/stickers/project-status/group-50.png +0 -0
- package/apps/studio/stickers/project-status/group-51.png +0 -0
- package/apps/studio/stickers/project-status/group-52.png +0 -0
- package/apps/studio/stickers/project-status/group-53.png +0 -0
- package/apps/studio/stickers/project-status/group-54.png +0 -0
- package/apps/studio/stickers/project-status/group-55.png +0 -0
- package/apps/studio/stickers/project-status/group-56.png +0 -0
- package/apps/studio/stickers/project-status/group-57.png +0 -0
- package/apps/studio/stickers/project-status/group-58.png +0 -0
- package/apps/studio/stickers/project-status/group-59.png +0 -0
- package/apps/studio/stickers/project-status/group-60.png +0 -0
- package/apps/studio/stickers/project-status/group-61.png +0 -0
- package/apps/studio/stickers/project-status/group-62.png +0 -0
- package/apps/studio/stickers/project-status/group-63.png +0 -0
- package/apps/studio/stickers/project-status/group-64.png +0 -0
- package/apps/studio/stickers/project-status/group-65.png +0 -0
- package/apps/studio/stickers/project-status/group-66.png +0 -0
- package/apps/studio/stickers/project-status/group-67.png +0 -0
- package/apps/studio/stickers/project-status/group-68.png +0 -0
- package/apps/studio/stickers/project-status/group-69.png +0 -0
- package/apps/studio/stickers/project-status/group-70.png +0 -0
- package/apps/studio/stickers/project-status/group-71.png +0 -0
- package/apps/studio/stickers/project-status/group-72.png +0 -0
- package/apps/studio/stickers/project-status/group-73.png +0 -0
- package/apps/studio/stickers/project-status/group-74.png +0 -0
- package/apps/studio/stickers/project-status/group-75.png +0 -0
- package/apps/studio/stickers/project-status/group-76.png +0 -0
- package/apps/studio/stickers/project-status/group-77.png +0 -0
- package/apps/studio/stickers/project-status/group-78.png +0 -0
- package/apps/studio/stickers/project-status/group-79.png +0 -0
- package/apps/studio/stickers/project-status/group-80.png +0 -0
- package/apps/studio/stickers/project-status/group-81.png +0 -0
- package/apps/studio/stickers/project-status/group-82.png +0 -0
- package/apps/studio/stickers/project-status/group-83.png +0 -0
- package/apps/studio/stickers/project-status/group-84.png +0 -0
- package/apps/studio/stickers/project-status/group-85.png +0 -0
- package/apps/studio/stickers/project-status/group-86.png +0 -0
- package/apps/studio/stickers/project-status/group-87.png +0 -0
- package/apps/studio/stickers/project-status/group-88.png +0 -0
- package/apps/studio/stickers/project-status/group-89.png +0 -0
- package/apps/studio/stickers/project-status/group-90.png +0 -0
- package/apps/studio/stickers/project-status/group-91.png +0 -0
- package/apps/studio/stickers/project-status/group-92.png +0 -0
- package/apps/studio/stickers/project-status/group-93.png +0 -0
- package/apps/studio/stickers/project-status/group-94.png +0 -0
- package/apps/studio/stickers/project-status/group-96.png +0 -0
- package/apps/studio/stickers/project-status/group-97.png +0 -0
- package/apps/studio/stickers/project-status/group-98.png +0 -0
- package/apps/studio/stickers/project-status/group-99.png +0 -0
- package/apps/studio/stickers/project-status/manifest.json +328 -0
- package/apps/studio/test/_helpers.ts +15 -2
- package/apps/studio/test/acp-bootstrap-brief.test.ts +11 -0
- package/apps/studio/test/acp-bridge.test.ts +34 -1
- package/apps/studio/test/acp-commands.test.ts +2 -1
- package/apps/studio/test/acp-env.test.ts +30 -0
- package/apps/studio/test/acp-plugin-bootstrap.test.ts +23 -57
- package/apps/studio/test/acp-session-plugins.test.ts +72 -1
- package/apps/studio/test/annotation-strokes-command.test.ts +17 -0
- package/apps/studio/test/annotations-layer.test.ts +174 -0
- package/apps/studio/test/asset-api.test.ts +3 -3
- package/apps/studio/test/canvas-edit.test.ts +17 -6
- package/apps/studio/test/canvas-origin-gate.test.ts +50 -0
- package/apps/studio/test/csp-canvas-shell.test.ts +11 -0
- package/apps/studio/test/csrf-write-guard.test.ts +23 -1
- package/apps/studio/test/design-setup-readiness.test.ts +155 -0
- package/apps/studio/test/dynamic-text-edit.test.ts +162 -0
- package/apps/studio/test/edit-source-command.test.ts +23 -0
- package/apps/studio/test/element-structural-edit.test.ts +154 -0
- package/apps/studio/test/fixtures/fake-claude-auth.mjs +13 -0
- package/apps/studio/test/footage-store.test.ts +100 -0
- package/apps/studio/test/generate-route.test.ts +106 -0
- package/apps/studio/test/git-branches.test.ts +97 -0
- package/apps/studio/test/github-api.test.ts +56 -1
- package/apps/studio/test/import-asset-browser.test.ts +64 -0
- package/apps/studio/test/import-asset-route.test.ts +71 -0
- package/apps/studio/test/import-asset.test.ts +399 -0
- package/apps/studio/test/import-brand-browser.test.ts +88 -0
- package/apps/studio/test/import-brand-route.test.ts +120 -0
- package/apps/studio/test/import-brand.test.ts +206 -0
- package/apps/studio/test/import-tokens.test.ts +722 -0
- package/apps/studio/test/input-router.test.ts +33 -0
- package/apps/studio/test/inspect-script-syntax.test.ts +53 -0
- package/apps/studio/test/media-commit-chain.test.ts +210 -0
- package/apps/studio/test/photo-bg-remove-validation.test.ts +71 -0
- package/apps/studio/test/photo-canvas-bundle.test.ts +61 -0
- package/apps/studio/test/photo-edit-api.test.ts +201 -0
- package/apps/studio/test/photo-filters.test.ts +149 -0
- package/apps/studio/test/photo-pipeline.test.ts +106 -0
- package/apps/studio/test/photo-taxonomy.test.ts +96 -0
- package/apps/studio/test/read-annotations.test.ts +164 -0
- package/apps/studio/test/readiness.test.ts +13 -11
- package/apps/studio/test/scaffold-design.test.ts +122 -0
- package/apps/studio/test/shell-importmap.test.ts +49 -0
- package/apps/studio/test/text-editability-stamp.test.ts +131 -0
- package/apps/studio/test/timeline-parse.test.ts +57 -0
- package/apps/studio/test/tool-palette-insert-anchor.test.ts +5 -3
- package/apps/studio/test/tour-overlay.test.tsx +16 -0
- package/apps/studio/test/undo-stack.test.ts +33 -0
- package/apps/studio/test/video-asset.test.ts +5 -5
- package/apps/studio/test/video-render-bridge.test.ts +23 -1
- package/apps/studio/text-caret.ts +239 -0
- package/apps/studio/tool-palette.tsx +49 -21
- package/apps/studio/ui-prefs.ts +130 -0
- package/apps/studio/undo-stack.ts +94 -2
- package/apps/studio/use-annotation-resize.tsx +4 -4
- package/apps/studio/use-canvas-media-drop.tsx +40 -1
- package/apps/studio/use-chrome-visibility.tsx +8 -2
- package/apps/studio/use-selection-set.tsx +21 -0
- package/apps/studio/video-comp.tsx +48 -7
- package/apps/studio/whats-new.json +149 -0
- package/apps/studio/ws.ts +5 -0
- package/cli/bin/maude.mjs +6 -6
- package/cli/commands/design.mjs +67 -3
- package/cli/lib/gitignore-block.mjs +2 -0
- package/cli/lib/pkg-root.mjs +107 -0
- package/cli/lib/pkg-root.test.mjs +79 -0
- package/cli/lib/reconstruct-toolset.test.mjs +194 -0
- package/cli/lib/update-check.mjs +8 -0
- package/package.json +13 -11
- package/plugins/design/dependencies.json +18 -0
- package/plugins/design/templates/_shell.html +1 -0
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
// generation/gemini.test.ts — the Gemini adapter + download localizer proven
|
|
2
|
+
// against a stubbed HTTP response (no real key, no network). A 1x1 PNG's base64
|
|
3
|
+
// round-trips: adapter → GenResult.bytes → localizeGenAsset → a saveAsset call
|
|
4
|
+
// that lands `assets/<sha8>.png`.
|
|
5
|
+
|
|
6
|
+
import { afterEach, describe, expect, test } from 'bun:test';
|
|
7
|
+
|
|
8
|
+
import { createGeminiAdapter, extractOperationName, extractVideoUri } from './adapters/gemini.ts';
|
|
9
|
+
import { localizeGenAsset } from './download.ts';
|
|
10
|
+
import { createAdapter, providersForModality } from './registry.ts';
|
|
11
|
+
import type { AdapterContext } from './types.ts';
|
|
12
|
+
|
|
13
|
+
// A real 1x1 transparent PNG (so a magic-byte sniff downstream would accept it).
|
|
14
|
+
const PNG_1x1_B64 =
|
|
15
|
+
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==';
|
|
16
|
+
|
|
17
|
+
const realFetch = globalThis.fetch;
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
globalThis.fetch = realFetch;
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
function stubFetch(body: unknown, status = 200): void {
|
|
23
|
+
globalThis.fetch = (async () =>
|
|
24
|
+
new Response(JSON.stringify(body), {
|
|
25
|
+
status,
|
|
26
|
+
headers: { 'content-type': 'application/json' },
|
|
27
|
+
})) as typeof fetch;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function ctxWith(overrides: Partial<AdapterContext> = {}): AdapterContext {
|
|
31
|
+
return {
|
|
32
|
+
apiKey: 'AIza-test-key',
|
|
33
|
+
localize: async () => 'assets/stub.png',
|
|
34
|
+
...overrides,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe('gemini adapter', () => {
|
|
39
|
+
test('a successful generateContent yields an already-done Job with PNG bytes', async () => {
|
|
40
|
+
stubFetch({
|
|
41
|
+
candidates: [
|
|
42
|
+
{ content: { parts: [{ inlineData: { mimeType: 'image/png', data: PNG_1x1_B64 } }] } },
|
|
43
|
+
],
|
|
44
|
+
});
|
|
45
|
+
const adapter = createGeminiAdapter(ctxWith());
|
|
46
|
+
const job = await adapter.submit({
|
|
47
|
+
modality: 'image',
|
|
48
|
+
provider: 'gemini',
|
|
49
|
+
prompt: 'a red circle',
|
|
50
|
+
});
|
|
51
|
+
const res = await job.result();
|
|
52
|
+
expect(job.status()).toBe('done');
|
|
53
|
+
expect(res.assets).toHaveLength(1);
|
|
54
|
+
expect(res.assets[0].kind).toBe('image');
|
|
55
|
+
expect(res.assets[0].bytes?.byteLength).toBeGreaterThan(0);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('a missing key fails fast without a network call', async () => {
|
|
59
|
+
let called = false;
|
|
60
|
+
globalThis.fetch = (async () => {
|
|
61
|
+
called = true;
|
|
62
|
+
return new Response('{}');
|
|
63
|
+
}) as typeof fetch;
|
|
64
|
+
const adapter = createGeminiAdapter(ctxWith({ apiKey: null }));
|
|
65
|
+
const job = await adapter.submit({ modality: 'image', provider: 'gemini', prompt: 'x' });
|
|
66
|
+
await expect(job.result()).rejects.toThrow(/no Google Gemini key/);
|
|
67
|
+
expect(called).toBe(false);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('an HTTP error surfaces the provider message, never the key', async () => {
|
|
71
|
+
stubFetch({ error: { message: 'quota exceeded' } }, 429);
|
|
72
|
+
const adapter = createGeminiAdapter(ctxWith());
|
|
73
|
+
const job = await adapter.submit({ modality: 'image', provider: 'gemini', prompt: 'x' });
|
|
74
|
+
await expect(job.result()).rejects.toThrow(/quota exceeded/);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('a response with no image part is an error', async () => {
|
|
78
|
+
stubFetch({ candidates: [{ content: { parts: [{ text: 'I cannot do that' }] } }] });
|
|
79
|
+
const adapter = createGeminiAdapter(ctxWith());
|
|
80
|
+
const job = await adapter.submit({ modality: 'image', provider: 'gemini', prompt: 'x' });
|
|
81
|
+
await expect(job.result()).rejects.toThrow(/no image/);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Task 1.2 — the maskless-edit flow reads the source asset into an inlineData
|
|
85
|
+
// part alongside the prompt (via the host-provided readSourceAsset).
|
|
86
|
+
test('an edit request sends the source image as an inlineData part', async () => {
|
|
87
|
+
let sentBody: {
|
|
88
|
+
contents?: Array<{
|
|
89
|
+
parts?: Array<{ text?: string; inlineData?: { mimeType?: string; data?: string } }>;
|
|
90
|
+
}>;
|
|
91
|
+
} | null = null;
|
|
92
|
+
globalThis.fetch = (async (_url: string, init: RequestInit) => {
|
|
93
|
+
sentBody = JSON.parse(String(init.body));
|
|
94
|
+
return new Response(
|
|
95
|
+
JSON.stringify({
|
|
96
|
+
candidates: [
|
|
97
|
+
{ content: { parts: [{ inlineData: { mimeType: 'image/png', data: PNG_1x1_B64 } }] } },
|
|
98
|
+
],
|
|
99
|
+
}),
|
|
100
|
+
{ status: 200, headers: { 'content-type': 'application/json' } }
|
|
101
|
+
);
|
|
102
|
+
}) as typeof fetch;
|
|
103
|
+
const adapter = createGeminiAdapter(
|
|
104
|
+
ctxWith({
|
|
105
|
+
readSourceAsset: async () => ({
|
|
106
|
+
bytes: new Uint8Array([137, 80, 78, 71]),
|
|
107
|
+
mime: 'image/png',
|
|
108
|
+
}),
|
|
109
|
+
})
|
|
110
|
+
);
|
|
111
|
+
const job = await adapter.submit({
|
|
112
|
+
modality: 'image',
|
|
113
|
+
provider: 'gemini',
|
|
114
|
+
prompt: 'make the sky purple',
|
|
115
|
+
sourceAsset: 'assets/deadbeef.png',
|
|
116
|
+
});
|
|
117
|
+
await job.result();
|
|
118
|
+
const parts = sentBody?.contents?.[0]?.parts ?? [];
|
|
119
|
+
// Source image FIRST, then the instruction text (Gemini's edit order).
|
|
120
|
+
expect(parts[0]?.inlineData?.mimeType).toBe('image/png');
|
|
121
|
+
expect(typeof parts[0]?.inlineData?.data).toBe('string');
|
|
122
|
+
expect(parts[1]?.text).toBe('make the sky purple');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('an edit request with no source-asset reader fails fast', async () => {
|
|
126
|
+
let called = false;
|
|
127
|
+
globalThis.fetch = (async () => {
|
|
128
|
+
called = true;
|
|
129
|
+
return new Response('{}');
|
|
130
|
+
}) as typeof fetch;
|
|
131
|
+
const adapter = createGeminiAdapter(ctxWith()); // no readSourceAsset
|
|
132
|
+
const job = await adapter.submit({
|
|
133
|
+
modality: 'image',
|
|
134
|
+
provider: 'gemini',
|
|
135
|
+
prompt: 'edit',
|
|
136
|
+
sourceAsset: 'assets/deadbeef.png',
|
|
137
|
+
});
|
|
138
|
+
await expect(job.result()).rejects.toThrow(/source-asset access/);
|
|
139
|
+
expect(called).toBe(false);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('an edit whose source is not an image is rejected', async () => {
|
|
143
|
+
const adapter = createGeminiAdapter(
|
|
144
|
+
ctxWith({
|
|
145
|
+
readSourceAsset: async () => ({ bytes: new Uint8Array([0, 0, 0]), mime: 'video/mp4' }),
|
|
146
|
+
})
|
|
147
|
+
);
|
|
148
|
+
const job = await adapter.submit({
|
|
149
|
+
modality: 'image',
|
|
150
|
+
provider: 'gemini',
|
|
151
|
+
prompt: 'edit',
|
|
152
|
+
sourceAsset: 'assets/deadbeef.mp4',
|
|
153
|
+
});
|
|
154
|
+
await expect(job.result()).rejects.toThrow(/not an image/);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// security fan-out F2 — an env-poisoned non-default base is refused before the
|
|
158
|
+
// key-bearing request (unless MAUDE_GEN_ALLOW_CUSTOM_BASE is opted in).
|
|
159
|
+
test('a non-allowlisted MAUDE_GEMINI_API_BASE is refused without touching the network', async () => {
|
|
160
|
+
const prevBase = process.env.MAUDE_GEMINI_API_BASE;
|
|
161
|
+
const prevAllow = process.env.MAUDE_GEN_ALLOW_CUSTOM_BASE;
|
|
162
|
+
process.env.MAUDE_GEMINI_API_BASE = 'https://evil.example.com/v1beta';
|
|
163
|
+
delete process.env.MAUDE_GEN_ALLOW_CUSTOM_BASE;
|
|
164
|
+
// Re-import the module so the module-level base constant re-reads the env.
|
|
165
|
+
const mod = await import(`./adapters/gemini.ts?evilbase=${Date.now()}`);
|
|
166
|
+
let called = false;
|
|
167
|
+
globalThis.fetch = (async () => {
|
|
168
|
+
called = true;
|
|
169
|
+
return new Response('{}');
|
|
170
|
+
}) as typeof fetch;
|
|
171
|
+
try {
|
|
172
|
+
const adapter = mod.createGeminiAdapter(ctxWith());
|
|
173
|
+
const job = await adapter.submit({ modality: 'image', provider: 'gemini', prompt: 'x' });
|
|
174
|
+
await expect(job.result()).rejects.toThrow(/not allowlisted/);
|
|
175
|
+
expect(called).toBe(false);
|
|
176
|
+
} finally {
|
|
177
|
+
if (prevBase === undefined) delete process.env.MAUDE_GEMINI_API_BASE;
|
|
178
|
+
else process.env.MAUDE_GEMINI_API_BASE = prevBase;
|
|
179
|
+
if (prevAllow === undefined) delete process.env.MAUDE_GEN_ALLOW_CUSTOM_BASE;
|
|
180
|
+
else process.env.MAUDE_GEN_ALLOW_CUSTOM_BASE = prevAllow;
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// security fan-out F1 — an oversized provider response is rejected before it
|
|
185
|
+
// is fully materialized + base64-decoded in RAM (OOM guard).
|
|
186
|
+
test('an over-cap response body is rejected (RAM DoS guard)', async () => {
|
|
187
|
+
const prev = process.env.MAUDE_GEMINI_MAX_RESPONSE_BYTES;
|
|
188
|
+
// The cap has a 1 MB floor, so drive the test with a >1 MB body at the floor.
|
|
189
|
+
process.env.MAUDE_GEMINI_MAX_RESPONSE_BYTES = String(1024 * 1024);
|
|
190
|
+
const mod = await import(`./adapters/gemini.ts?cap=${Date.now()}`);
|
|
191
|
+
globalThis.fetch = (async () =>
|
|
192
|
+
new Response('x'.repeat(2 * 1024 * 1024), {
|
|
193
|
+
status: 200,
|
|
194
|
+
headers: { 'content-type': 'application/json' },
|
|
195
|
+
})) as typeof fetch;
|
|
196
|
+
try {
|
|
197
|
+
const adapter = mod.createGeminiAdapter(ctxWith());
|
|
198
|
+
const job = await adapter.submit({ modality: 'image', provider: 'gemini', prompt: 'x' });
|
|
199
|
+
await expect(job.result()).rejects.toThrow(/too large|exceeded/);
|
|
200
|
+
} finally {
|
|
201
|
+
if (prev === undefined) delete process.env.MAUDE_GEMINI_MAX_RESPONSE_BYTES;
|
|
202
|
+
else process.env.MAUDE_GEMINI_MAX_RESPONSE_BYTES = prev;
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
describe('download localizer', () => {
|
|
208
|
+
test('inline bytes land via saveAsset and return the rel path', async () => {
|
|
209
|
+
const saved: Uint8Array[] = [];
|
|
210
|
+
const rel = await localizeGenAsset(
|
|
211
|
+
{ kind: 'image', mime: 'image/png', bytes: new Uint8Array([137, 80, 78, 71]) },
|
|
212
|
+
{
|
|
213
|
+
saveAsset: async (bytes) => {
|
|
214
|
+
saved.push(bytes);
|
|
215
|
+
return { ok: true, path: 'assets/deadbeef.png' };
|
|
216
|
+
},
|
|
217
|
+
}
|
|
218
|
+
);
|
|
219
|
+
expect(rel).toBe('assets/deadbeef.png');
|
|
220
|
+
expect(saved).toHaveLength(1);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test('a rejected save throws', async () => {
|
|
224
|
+
await expect(
|
|
225
|
+
localizeGenAsset(
|
|
226
|
+
{ kind: 'image', mime: 'image/png', bytes: new Uint8Array([1]) },
|
|
227
|
+
{ saveAsset: async () => ({ ok: false, error: 'not an image' }) }
|
|
228
|
+
)
|
|
229
|
+
).rejects.toThrow(/rejected on save/);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test('a URL result fails loud in Phase 0 (no unhardened egress)', async () => {
|
|
233
|
+
await expect(
|
|
234
|
+
localizeGenAsset(
|
|
235
|
+
{ kind: 'image', mime: 'image/png', url: 'https://cdn.example/x.png' },
|
|
236
|
+
{ saveAsset: async () => ({ ok: true, path: 'assets/x.png' }) }
|
|
237
|
+
)
|
|
238
|
+
).rejects.toThrow(/Phase 1/);
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
describe('registry', () => {
|
|
243
|
+
test('gemini is registered for image and instantiable', () => {
|
|
244
|
+
expect(providersForModality('image').some((p) => p.id === 'gemini')).toBe(true);
|
|
245
|
+
const adapter = createAdapter('gemini', ctxWith());
|
|
246
|
+
expect(adapter.descriptor.id).toBe('gemini');
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test('an unknown provider throws', () => {
|
|
250
|
+
expect(() => createAdapter('nope', ctxWith())).toThrow(/unknown provider/);
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
describe('veo video (Task 3.1 — async operation lifecycle)', () => {
|
|
255
|
+
// A minimal MP4 (ftyp box) so a magic-byte sniff downstream would accept it.
|
|
256
|
+
const MP4 = new Uint8Array([
|
|
257
|
+
0x00, 0x00, 0x00, 0x18, 0x66, 0x74, 0x79, 0x70, 0x6d, 0x70, 0x34, 0x32, 0, 0, 0, 0, 0x6d, 0x70,
|
|
258
|
+
0x34, 0x32, 0x69, 0x73, 0x6f, 0x6d,
|
|
259
|
+
]);
|
|
260
|
+
|
|
261
|
+
test('extractOperationName reads the start-response name', () => {
|
|
262
|
+
expect(extractOperationName({ name: 'operations/abc' })).toBe('operations/abc');
|
|
263
|
+
expect(extractOperationName({})).toBeNull();
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test('extractVideoUri walks nested shapes for the first https uri', () => {
|
|
267
|
+
expect(
|
|
268
|
+
extractVideoUri({
|
|
269
|
+
response: {
|
|
270
|
+
generateVideoResponse: {
|
|
271
|
+
generatedSamples: [{ video: { uri: 'https://generativelanguage.googleapis.com/v/1' } }],
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
})
|
|
275
|
+
).toBe('https://generativelanguage.googleapis.com/v/1');
|
|
276
|
+
expect(
|
|
277
|
+
extractVideoUri({
|
|
278
|
+
response: {
|
|
279
|
+
generatedVideos: [
|
|
280
|
+
{ video: { fileUri: 'https://generativelanguage.googleapis.com/v/2' } },
|
|
281
|
+
],
|
|
282
|
+
},
|
|
283
|
+
})
|
|
284
|
+
).toBe('https://generativelanguage.googleapis.com/v/2');
|
|
285
|
+
expect(extractVideoUri({ response: {} })).toBeNull();
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test('submit(video) drives start → poll(done) → download and yields MP4 bytes', async () => {
|
|
289
|
+
process.env.MAUDE_VEO_POLL_INTERVAL_MS = '1';
|
|
290
|
+
const calls: string[] = [];
|
|
291
|
+
globalThis.fetch = (async (url: string) => {
|
|
292
|
+
calls.push(String(url));
|
|
293
|
+
if (String(url).includes(':predictLongRunning'))
|
|
294
|
+
return new Response(JSON.stringify({ name: 'operations/xyz' }), {
|
|
295
|
+
status: 200,
|
|
296
|
+
headers: { 'content-type': 'application/json' },
|
|
297
|
+
});
|
|
298
|
+
if (String(url).includes('operations/xyz'))
|
|
299
|
+
return new Response(
|
|
300
|
+
JSON.stringify({
|
|
301
|
+
done: true,
|
|
302
|
+
response: {
|
|
303
|
+
generatedSamples: [
|
|
304
|
+
{
|
|
305
|
+
video: {
|
|
306
|
+
uri: 'https://generativelanguage.googleapis.com/v1beta/files/x:download',
|
|
307
|
+
},
|
|
308
|
+
},
|
|
309
|
+
],
|
|
310
|
+
},
|
|
311
|
+
}),
|
|
312
|
+
{ status: 200, headers: { 'content-type': 'application/json' } }
|
|
313
|
+
);
|
|
314
|
+
// the video download
|
|
315
|
+
return new Response(MP4, { status: 200, headers: { 'content-type': 'video/mp4' } });
|
|
316
|
+
}) as typeof fetch;
|
|
317
|
+
|
|
318
|
+
const adapter = createGeminiAdapter(ctxWith());
|
|
319
|
+
const job = await adapter.submit({ modality: 'video', provider: 'gemini', prompt: 'a wave' });
|
|
320
|
+
const res = await job.result();
|
|
321
|
+
delete process.env.MAUDE_VEO_POLL_INTERVAL_MS;
|
|
322
|
+
expect(res.assets[0].kind).toBe('video');
|
|
323
|
+
expect(res.assets[0].bytes?.byteLength).toBe(MP4.byteLength);
|
|
324
|
+
expect(calls.some((u) => u.includes(':predictLongRunning'))).toBe(true);
|
|
325
|
+
expect(calls.some((u) => u.includes('operations/xyz'))).toBe(true);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
test('a video URI off the Google host is rejected (no key exfil)', async () => {
|
|
329
|
+
process.env.MAUDE_VEO_POLL_INTERVAL_MS = '1';
|
|
330
|
+
globalThis.fetch = (async (url: string) => {
|
|
331
|
+
if (String(url).includes(':predictLongRunning'))
|
|
332
|
+
return new Response(JSON.stringify({ name: 'operations/xyz' }), { status: 200 });
|
|
333
|
+
return new Response(
|
|
334
|
+
JSON.stringify({ done: true, response: { video: { uri: 'https://evil.example/v.mp4' } } }),
|
|
335
|
+
{ status: 200 }
|
|
336
|
+
);
|
|
337
|
+
}) as typeof fetch;
|
|
338
|
+
const job = await createGeminiAdapter(ctxWith()).submit({
|
|
339
|
+
modality: 'video',
|
|
340
|
+
provider: 'gemini',
|
|
341
|
+
prompt: 'x',
|
|
342
|
+
});
|
|
343
|
+
await expect(job.result()).rejects.toThrow(/host not allowlisted/);
|
|
344
|
+
delete process.env.MAUDE_VEO_POLL_INTERVAL_MS;
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
test('gemini is now registered for the video modality', () => {
|
|
348
|
+
expect(providersForModality('video').some((p) => p.id === 'gemini')).toBe(true);
|
|
349
|
+
});
|
|
350
|
+
});
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// generation/jobs.test.ts — the generation job queue: enqueue → done/failed
|
|
2
|
+
// transitions, history ledger, backpressure. Uses a tiny fake Bus + a tmp
|
|
3
|
+
// designRoot so no real server or provider is involved.
|
|
4
|
+
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
|
6
|
+
import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
|
7
|
+
import { tmpdir } from 'node:os';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
|
|
10
|
+
import { createGenerationJobQueue, GenerationQueueFullError } from './jobs.ts';
|
|
11
|
+
|
|
12
|
+
interface FakeBus {
|
|
13
|
+
emit(evt: string, payload?: unknown): void;
|
|
14
|
+
on(evt: string, fn: (p: unknown) => void): void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function fakeBus(): { bus: FakeBus; events: Array<{ evt: string; payload: unknown }> } {
|
|
18
|
+
const events: Array<{ evt: string; payload: unknown }> = [];
|
|
19
|
+
return {
|
|
20
|
+
events,
|
|
21
|
+
bus: { emit: (evt, payload) => events.push({ evt, payload }), on: () => {} },
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let dir: string;
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
dir = mkdtempSync(join(tmpdir(), 'maude-genq-'));
|
|
28
|
+
});
|
|
29
|
+
afterEach(() => rmSync(dir, { recursive: true, force: true }));
|
|
30
|
+
|
|
31
|
+
describe('generation job queue', () => {
|
|
32
|
+
test('a successful job transitions queued→running→done and records assets', async () => {
|
|
33
|
+
const { bus, events } = fakeBus();
|
|
34
|
+
const q = createGenerationJobQueue(bus as never, dir);
|
|
35
|
+
const { id, result } = q.enqueue({
|
|
36
|
+
provider: 'gemini',
|
|
37
|
+
modality: 'image',
|
|
38
|
+
model: 'gemini-2.5-flash-image',
|
|
39
|
+
run: async () => ({ assets: ['assets/abc12345.png'], usage: { ms: 5 } }),
|
|
40
|
+
});
|
|
41
|
+
const job = await result;
|
|
42
|
+
expect(job.id).toBe(id);
|
|
43
|
+
expect(job.status).toBe('done');
|
|
44
|
+
expect(job.assets).toEqual(['assets/abc12345.png']);
|
|
45
|
+
const statuses = events
|
|
46
|
+
.filter((e) => e.evt === 'generate:job')
|
|
47
|
+
.map((e) => (e.payload as { status: string }).status);
|
|
48
|
+
expect(statuses).toContain('queued');
|
|
49
|
+
expect(statuses).toContain('running');
|
|
50
|
+
expect(statuses).toContain('done');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('a failing run marks the job failed and captures the error', async () => {
|
|
54
|
+
const { bus } = fakeBus();
|
|
55
|
+
const q = createGenerationJobQueue(bus as never, dir);
|
|
56
|
+
const { result } = q.enqueue({
|
|
57
|
+
provider: 'gemini',
|
|
58
|
+
modality: 'image',
|
|
59
|
+
run: async () => {
|
|
60
|
+
throw new Error('provider exploded');
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
await expect(result).rejects.toThrow(/provider exploded/);
|
|
64
|
+
const job = q.list()[0];
|
|
65
|
+
expect(job.status).toBe('failed');
|
|
66
|
+
expect(job.error).toMatch(/provider exploded/);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('finished jobs persist to the _generate-history.json ledger', async () => {
|
|
70
|
+
const { bus } = fakeBus();
|
|
71
|
+
const q = createGenerationJobQueue(bus as never, dir);
|
|
72
|
+
await q.enqueue({
|
|
73
|
+
provider: 'gemini',
|
|
74
|
+
modality: 'image',
|
|
75
|
+
run: async () => ({ assets: ['assets/deadbeef.png'] }),
|
|
76
|
+
}).result;
|
|
77
|
+
const ledger = JSON.parse(readFileSync(join(dir, '_generate-history.json'), 'utf8'));
|
|
78
|
+
expect(Array.isArray(ledger)).toBe(true);
|
|
79
|
+
expect(ledger[0].assets).toEqual(['assets/deadbeef.png']);
|
|
80
|
+
expect(ledger[0].status).toBe('done');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('the ledger is re-seeded on a fresh queue (survives restart)', async () => {
|
|
84
|
+
const { bus } = fakeBus();
|
|
85
|
+
const q1 = createGenerationJobQueue(bus as never, dir);
|
|
86
|
+
await q1.enqueue({
|
|
87
|
+
provider: 'gemini',
|
|
88
|
+
modality: 'image',
|
|
89
|
+
run: async () => ({ assets: ['assets/seed0001.png'] }),
|
|
90
|
+
}).result;
|
|
91
|
+
const q2 = createGenerationJobQueue(bus as never, dir);
|
|
92
|
+
expect(q2.loadHistory().some((h) => h.assets?.includes('assets/seed0001.png'))).toBe(true);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('backpressure — a flood past MAX_PENDING throws GenerationQueueFullError', async () => {
|
|
96
|
+
const prev = process.env.MAUDE_GENERATE_MAX_QUEUED;
|
|
97
|
+
const prevConc = process.env.MAUDE_GENERATE_MAX_CONCURRENT;
|
|
98
|
+
process.env.MAUDE_GENERATE_MAX_QUEUED = '2';
|
|
99
|
+
process.env.MAUDE_GENERATE_MAX_CONCURRENT = '1';
|
|
100
|
+
try {
|
|
101
|
+
const { bus } = fakeBus();
|
|
102
|
+
const q = createGenerationJobQueue(bus as never, dir);
|
|
103
|
+
// A run that never resolves until we let it, so jobs stay pending.
|
|
104
|
+
let release!: () => void;
|
|
105
|
+
const gate = new Promise<void>((r) => {
|
|
106
|
+
release = r;
|
|
107
|
+
});
|
|
108
|
+
const mk = () =>
|
|
109
|
+
q.enqueue({
|
|
110
|
+
provider: 'gemini',
|
|
111
|
+
modality: 'image',
|
|
112
|
+
run: async () => {
|
|
113
|
+
await gate;
|
|
114
|
+
return { assets: [] };
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
mk(); // running (holds the single concurrency slot)
|
|
118
|
+
mk(); // queued
|
|
119
|
+
expect(() => mk()).toThrow(GenerationQueueFullError); // over the cap
|
|
120
|
+
release();
|
|
121
|
+
} finally {
|
|
122
|
+
if (prev === undefined) delete process.env.MAUDE_GENERATE_MAX_QUEUED;
|
|
123
|
+
else process.env.MAUDE_GENERATE_MAX_QUEUED = prev;
|
|
124
|
+
if (prevConc === undefined) delete process.env.MAUDE_GENERATE_MAX_CONCURRENT;
|
|
125
|
+
else process.env.MAUDE_GENERATE_MAX_CONCURRENT = prevConc;
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
});
|