@hyperframes/gcp-cloud-run 0.8.230 → 0.9.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/dist/server.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/server.ts
2
- import { existsSync as existsSync3, mkdirSync as mkdirSync3, mkdtempSync as mkdtempSync2, readFileSync, rmSync as rmSync3, statSync as statSync3 } from "node:fs";
2
+ import { mkdirSync as mkdirSync3, mkdtempSync as mkdtempSync2, rmSync as rmSync3 } from "node:fs";
3
3
  import { tmpdir as tmpdir2 } from "node:os";
4
4
  import { basename, extname, join as join2 } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
@@ -10,9 +10,7 @@ import {
10
10
  assemble,
11
11
  listPlanV2ArtifactsForTarget,
12
12
  materializePlanV2Target,
13
- plan,
14
13
  isPlanAudioArtifactPath,
15
- PLAN_AUDIO_RELATIVE_PATH,
16
14
  resolvePlanAudioPath,
17
15
  planV2WithPublisher,
18
16
  readPlanV2Manifest,
@@ -380,27 +378,26 @@ async function dispatch(event, deps) {
380
378
  function validatePlanProtocolShape(event) {
381
379
  const raw = event;
382
380
  const protocol = raw.PlanProtocol;
383
- if (protocol !== void 0 && protocol !== "v1" && protocol !== "v2") {
381
+ if (protocol !== void 0 && protocol !== "v2") {
384
382
  const error = new Error(
385
- `[handler] unsupported PlanProtocol ${JSON.stringify(protocol)}; expected "v1", "v2", or absent`
383
+ `[handler] unsupported PlanProtocol ${JSON.stringify(protocol)}; expected "v2" or absent`
386
384
  );
387
385
  error.name = "PLAN_PROTOCOL_UNSUPPORTED";
388
386
  throw error;
389
387
  }
390
388
  if (event.Action === "plan") return;
391
- const effectiveProtocol = protocol ?? "v2";
392
389
  const hasV1Locator = typeof raw.PlanGcsUri === "string";
393
390
  const hasV2Manifest = typeof raw.PlanV2ManifestGcsUri === "string";
394
391
  const hasV2Prefix = typeof raw.PlanV2ArtifactGcsPrefix === "string";
395
- const valid = effectiveProtocol === "v2" ? !hasV1Locator && hasV2Manifest && hasV2Prefix : hasV1Locator && !hasV2Manifest && !hasV2Prefix;
392
+ const valid = !hasV1Locator && hasV2Manifest && hasV2Prefix;
396
393
  if (!valid) {
397
394
  const error = new Error(
398
- `[handler] ${effectiveProtocol} ${event.Action} event has mixed or missing plan locators`
395
+ `[handler] v2 ${event.Action} event has mixed or missing plan locators`
399
396
  );
400
397
  error.name = "PLAN_PROTOCOL_UNSUPPORTED";
401
398
  throw error;
402
399
  }
403
- if (effectiveProtocol === "v2" && event.Action === "assemble" && event.AudioGcsUri !== null) {
400
+ if (event.Action === "assemble" && event.AudioGcsUri !== null) {
404
401
  const error = new Error("[handler] v2 assemble audio must be materialized from the manifest");
405
402
  error.name = "PLAN_PROTOCOL_UNSUPPORTED";
406
403
  throw error;
@@ -409,7 +406,7 @@ function validatePlanProtocolShape(event) {
409
406
  function normalizeTerminalErrorName(error) {
410
407
  if (!error || typeof error !== "object") return;
411
408
  const candidate = error;
412
- if (candidate.code === "PLAN_PROTOCOL_UNSUPPORTED" || candidate.code === "PLAN_TOO_LARGE" || candidate.code === "PLAN_V2_INTEGRITY_UNRECOVERABLE" || candidate.code === "FONT_FETCH_FAILED" || candidate.code === "FONT_FETCH_UNAVAILABLE" || candidate.code === "VIDEO_SOURCE_UNRENDERABLE" || candidate.code === "VIDEO_EXTRACTION_FAILED" || candidate.code === "INVALID_VIDEO_METADATA" || candidate.code === "CHUNK_EXCEEDS_WORKER_TIMEOUT") {
409
+ if (candidate.code === "PLAN_PROTOCOL_UNSUPPORTED" || candidate.code === "PLAN_V2_INTEGRITY_UNRECOVERABLE" || candidate.code === "FONT_FETCH_FAILED" || candidate.code === "FONT_FETCH_UNAVAILABLE" || candidate.code === "VIDEO_SOURCE_UNRENDERABLE" || candidate.code === "VIDEO_EXTRACTION_FAILED" || candidate.code === "INVALID_VIDEO_METADATA" || candidate.code === "CHUNK_EXCEEDS_WORKER_TIMEOUT") {
413
410
  candidate.name = candidate.code;
414
411
  }
415
412
  }
@@ -456,14 +453,14 @@ function summarizeEvent(event) {
456
453
  case "renderChunk":
457
454
  return {
458
455
  planProtocol: event.PlanProtocol ?? "v2",
459
- ...event.PlanProtocol !== "v1" ? { planV2ManifestGcsUri: event.PlanV2ManifestGcsUri } : { planGcsUri: event.PlanGcsUri },
456
+ planV2ManifestGcsUri: event.PlanV2ManifestGcsUri,
460
457
  chunkIndex: event.ChunkIndex,
461
458
  format: event.Format
462
459
  };
463
460
  case "assemble":
464
461
  return {
465
462
  planProtocol: event.PlanProtocol ?? "v2",
466
- ...event.PlanProtocol !== "v1" ? { planV2ManifestGcsUri: event.PlanV2ManifestGcsUri } : { planGcsUri: event.PlanGcsUri },
463
+ planV2ManifestGcsUri: event.PlanV2ManifestGcsUri,
467
464
  chunkCount: event.ChunkGcsUris.length,
468
465
  hasAudio: event.AudioGcsUri !== null,
469
466
  outputGcsUri: event.OutputGcsUri,
@@ -477,51 +474,6 @@ function primeChrome(deps) {
477
474
  process.env.PRODUCER_HEADLESS_SHELL_PATH = resolveChromeExecutablePath();
478
475
  }
479
476
  async function handlePlan(event, deps) {
480
- if (event.PlanProtocol !== "v1") {
481
- return handlePlanV2(event, deps);
482
- }
483
- const started = Date.now();
484
- const storage = deps?.storage ?? getStorage();
485
- const primitive = deps?.primitives?.plan ?? plan;
486
- primeChrome(deps);
487
- const work = mkdtempSync2(join2(deps?.tmpRoot ?? tmpdir2(), "hf-cr-plan-"));
488
- const projectArchive = join2(work, "project.tar.gz");
489
- const projectDir = join2(work, "project");
490
- const planDir = join2(work, "plan");
491
- try {
492
- await downloadGcsObjectToFile(storage, event.ProjectGcsUri, projectArchive);
493
- await untarDirectory(projectArchive, projectDir);
494
- const config = {
495
- ...event.Config
496
- };
497
- const result = await primitive(projectDir, config, planDir);
498
- const planTar = join2(work, "plan.tar.gz");
499
- await tarDirectory(planDir, planTar);
500
- const planTarUri = `${trimTrailingSlash2(event.PlanOutputGcsPrefix)}/plan.tar.gz`;
501
- const audioPath = join2(planDir, PLAN_AUDIO_RELATIVE_PATH);
502
- const hasAudio = existsSync3(audioPath) && statSync3(audioPath).size > 0;
503
- await uploadFileToGcs(storage, planTar, planTarUri, "application/gzip");
504
- return {
505
- Action: "plan",
506
- PlanGcsUri: planTarUri,
507
- PlanHash: result.planHash,
508
- ChunkCount: result.chunkCount,
509
- TotalFrames: result.totalFrames,
510
- Fps: result.fps,
511
- Width: result.width,
512
- Height: result.height,
513
- Format: result.format,
514
- HasAudio: hasAudio,
515
- AudioGcsUri: null,
516
- FfmpegVersion: result.ffmpegVersion,
517
- ProducerVersion: result.producerVersion,
518
- DurationMs: Date.now() - started
519
- };
520
- } finally {
521
- cleanupDir(work);
522
- }
523
- }
524
- async function handlePlanV2(event, deps) {
525
477
  const started = Date.now();
526
478
  const storage = deps?.storage ?? getStorage();
527
479
  const primitive = deps?.primitives?.planV2WithPublisher ?? planV2WithPublisher;
@@ -563,45 +515,6 @@ async function handlePlanV2(event, deps) {
563
515
  }
564
516
  }
565
517
  async function handleRenderChunk(event, deps) {
566
- if (event.PlanProtocol !== "v1") {
567
- return handleRenderChunkV2(event, deps);
568
- }
569
- const started = Date.now();
570
- const storage = deps?.storage ?? getStorage();
571
- const primitive = deps?.primitives?.renderChunk ?? renderChunk;
572
- primeChrome(deps);
573
- const work = mkdtempSync2(join2(deps?.tmpRoot ?? tmpdir2(), "hf-cr-chunk-"));
574
- const planTar = join2(work, "plan.tar.gz");
575
- const planDir = join2(work, "plan");
576
- try {
577
- await downloadGcsObjectToFile(storage, event.PlanGcsUri, planTar);
578
- await untarDirectory(planTar, planDir);
579
- verifyPlanHash(planDir, event.PlanHash);
580
- const chunkOutputBase = join2(
581
- work,
582
- event.Format === "png-sequence" ? `chunk-${pad(event.ChunkIndex)}` : `chunk-${pad(event.ChunkIndex)}${formatExtension(event.Format)}`
583
- );
584
- const result = await primitive(planDir, event.ChunkIndex, chunkOutputBase);
585
- const chunkUri = await uploadChunkOutput(
586
- storage,
587
- result,
588
- event.ChunkOutputGcsPrefix,
589
- event.ChunkIndex
590
- );
591
- return {
592
- Action: "renderChunk",
593
- ChunkGcsUri: chunkUri,
594
- ChunkIndex: event.ChunkIndex,
595
- Sha256: result.sha256,
596
- FramesEncoded: result.framesEncoded,
597
- CaptureMode: result.captureMode,
598
- DurationMs: Date.now() - started
599
- };
600
- } finally {
601
- cleanupDir(work);
602
- }
603
- }
604
- async function handleRenderChunkV2(event, deps) {
605
518
  const started = Date.now();
606
519
  const storage = deps?.storage ?? getStorage();
607
520
  const primitive = deps?.primitives?.renderChunk ?? renderChunk;
@@ -653,50 +566,6 @@ async function uploadChunkOutput(storage, result, prefix, chunkIndex) {
653
566
  return uri;
654
567
  }
655
568
  async function handleAssemble(event, deps) {
656
- if (event.PlanProtocol !== "v1") {
657
- return handleAssembleV2(event, deps);
658
- }
659
- const started = Date.now();
660
- const storage = deps?.storage ?? getStorage();
661
- const primitive = deps?.primitives?.assemble ?? assemble;
662
- const work = mkdtempSync2(join2(deps?.tmpRoot ?? tmpdir2(), "hf-cr-assemble-"));
663
- const planTar = join2(work, "plan.tar.gz");
664
- const planDir = join2(work, "plan");
665
- try {
666
- await downloadGcsObjectToFile(storage, event.PlanGcsUri, planTar);
667
- await untarDirectory(planTar, planDir);
668
- const chunkPaths = await downloadChunkObjects(storage, event.ChunkGcsUris, work, event.Format);
669
- let audioPath = null;
670
- const planAudio = resolvePlanAudioPath(planDir) ?? join2(planDir, PLAN_AUDIO_RELATIVE_PATH);
671
- if (existsSync3(planAudio) && statSync3(planAudio).size > 0) {
672
- audioPath = planAudio;
673
- } else if (event.AudioGcsUri) {
674
- audioPath = planAudio;
675
- await downloadGcsObjectToFile(storage, event.AudioGcsUri, audioPath);
676
- }
677
- const finalOutput = event.Format === "png-sequence" ? join2(work, "output-frames") : join2(work, `output${formatExtension(event.Format)}`);
678
- const result = await primitive(planDir, chunkPaths, audioPath, finalOutput, {
679
- cfr: event.Cfr === true
680
- });
681
- if (event.Format === "png-sequence") {
682
- const tarball = `${finalOutput}.tar.gz`;
683
- await tarDirectory(finalOutput, tarball);
684
- await uploadFileToGcs(storage, tarball, event.OutputGcsUri, "application/gzip");
685
- } else {
686
- await uploadFileToGcs(storage, finalOutput, event.OutputGcsUri);
687
- }
688
- return {
689
- Action: "assemble",
690
- OutputGcsUri: event.OutputGcsUri,
691
- FramesEncoded: result.framesEncoded,
692
- FileSize: result.fileSize,
693
- DurationMs: Date.now() - started
694
- };
695
- } finally {
696
- cleanupDir(work);
697
- }
698
- }
699
- async function handleAssembleV2(event, deps) {
700
569
  const started = Date.now();
701
570
  const storage = deps?.storage ?? getStorage();
702
571
  const primitive = deps?.primitives?.assemble ?? assemble;
@@ -815,10 +684,15 @@ function getEventGcsUris(event) {
815
684
  case "plan":
816
685
  return [event.ProjectGcsUri, event.PlanOutputGcsPrefix];
817
686
  case "renderChunk":
818
- return event.PlanProtocol !== "v1" ? [event.PlanV2ManifestGcsUri, event.PlanV2ArtifactGcsPrefix, event.ChunkOutputGcsPrefix] : [event.PlanGcsUri, event.ChunkOutputGcsPrefix];
687
+ return [
688
+ event.PlanV2ManifestGcsUri,
689
+ event.PlanV2ArtifactGcsPrefix,
690
+ event.ChunkOutputGcsPrefix
691
+ ];
819
692
  case "assemble":
820
693
  return [
821
- ...event.PlanProtocol !== "v1" ? [event.PlanV2ManifestGcsUri, event.PlanV2ArtifactGcsPrefix] : [event.PlanGcsUri],
694
+ event.PlanV2ManifestGcsUri,
695
+ event.PlanV2ArtifactGcsPrefix,
822
696
  ...event.ChunkGcsUris,
823
697
  event.OutputGcsUri,
824
698
  event.AudioGcsUri
@@ -863,26 +737,6 @@ function cleanupDir(dir) {
863
737
  } catch {
864
738
  }
865
739
  }
866
- function verifyPlanHash(planDir, expected) {
867
- const planJsonPath = join2(planDir, "plan.json");
868
- let parsed;
869
- try {
870
- parsed = JSON.parse(readFileSync(planJsonPath, "utf-8"));
871
- } catch (err) {
872
- const msg = err instanceof Error ? err.message : String(err);
873
- const error = new Error(`PLAN_HASH_MISMATCH: failed to read ${planJsonPath}: ${msg}`);
874
- error.name = "PLAN_HASH_MISMATCH";
875
- throw error;
876
- }
877
- const actual = parsed.planHash;
878
- if (typeof actual !== "string" || actual !== expected) {
879
- const error = new Error(
880
- `PLAN_HASH_MISMATCH: event PlanHash=${expected} did not match plan.json planHash=${String(actual)}`
881
- );
882
- error.name = "PLAN_HASH_MISMATCH";
883
- throw error;
884
- }
885
- }
886
740
  var NON_RETRYABLE_ERROR_NAMES = /* @__PURE__ */ new Set([
887
741
  // Handler-boundary guards.
888
742
  "GCS_URI_NOT_ALLOWED",
@@ -900,13 +754,11 @@ var NON_RETRYABLE_ERROR_NAMES = /* @__PURE__ */ new Set([
900
754
  // non-retryable list.
901
755
  "FormatNotSupportedInDistributedError",
902
756
  "NotMediaPayloadError",
903
- "PlanTooLargeError",
904
757
  "PlanProtocolUnsupportedError",
905
758
  "PlanV2IntegrityError",
906
759
  "RenderChunkValidationError",
907
760
  "FFMPEG_VERSION_MISMATCH",
908
761
  "FORMAT_NOT_SUPPORTED_IN_DISTRIBUTED",
909
- "PLAN_TOO_LARGE",
910
762
  "BROWSER_GPU_NOT_SOFTWARE",
911
763
  "FONT_FETCH_FAILED",
912
764
  "ChromeBinaryUnavailableError"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/gcp-cloud-run",
3
- "version": "0.8.230",
3
+ "version": "0.9.0",
4
4
  "description": "Google Cloud Run + Workflows adapter for HyperFrames distributed rendering — request handler, client-side SDK, and Terraform module.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -45,7 +45,7 @@
45
45
  "hono": "^4.6.0",
46
46
  "puppeteer-core": "^25.8.0",
47
47
  "tar": "^7.4.3",
48
- "@hyperframes/producer": "^0.8.230"
48
+ "@hyperframes/producer": "^0.9.0"
49
49
  },
50
50
  "devDependencies": {
51
51
  "@types/node": "^25.0.10",
@@ -7,13 +7,11 @@ const smoke = readFileSync(smokePath, "utf-8");
7
7
  const dockerfile = readFileSync(join(import.meta.dir, "../Dockerfile"), "utf-8");
8
8
 
9
9
  describe("GCP smoke ownership and protocol safety", () => {
10
- it("defaults to v2 and retains explicit v1/v2 protocol arguments", () => {
11
- expect(smoke).toContain('PROTOCOLS="${PROTOCOLS:-v2}"');
12
- expect(smoke).toContain("--protocols)");
10
+ it("runs only the Plan v2 protocol", () => {
11
+ expect(smoke).toContain('PROTOCOL="v2"');
12
+ expect(smoke).not.toContain("--protocols)");
13
13
  expect(smoke).toContain("PlanProtocol: $protocol");
14
- expect(smoke).toContain("decodedFramesEqual");
15
- expect(smoke).toContain("decodedAudioEqual");
16
- expect(smoke).toContain("normalizedMetadataEqual");
14
+ expect(smoke).not.toContain("PlanProtocol: v1");
17
15
  expect(smoke).toContain('.CaptureMode == "beginframe"');
18
16
  expect(smoke).toContain("expected every chunk to use beginframe");
19
17
  });
@@ -6,19 +6,12 @@ import { parse } from "yaml";
6
6
  type Step = Record<string, unknown>;
7
7
 
8
8
  const source = readFileSync(join(import.meta.dir, "workflow.yaml"), "utf-8");
9
- // Cloud Workflows expressions are valid to Google's parser but `${...}`
10
- // inside YAML flow collections is not valid generic YAML. Quote expressions
11
- // for structural parsing while preserving their text for contract assertions.
12
9
  const parseableSource = source.replace(/\$\{([^}]*)\}/g, (_match, expression: string) =>
13
10
  JSON.stringify(`\${${expression}}`),
14
11
  );
15
12
  const workflow = parse(parseableSource) as {
16
- main: {
17
- steps: Step[];
18
- };
19
- retryable: {
20
- steps: Step[];
21
- };
13
+ main: { steps: Step[] };
14
+ retryable: { steps: Step[] };
22
15
  };
23
16
 
24
17
  function namedStep(name: string, steps = workflow.main.steps): Record<string, unknown> {
@@ -28,121 +21,54 @@ function namedStep(name: string, steps = workflow.main.steps): Record<string, un
28
21
  throw new Error(`missing workflow step ${name}`);
29
22
  }
30
23
 
31
- function requestBody(stepName: string): Record<string, unknown> {
32
- const step = namedStep(stepName);
33
- const attempt = step.try as {
34
- args: {
35
- body: Record<string, unknown>;
36
- };
37
- };
38
- return attempt.args.body;
24
+ function requestBody(stepName: "plan" | "assemble"): Record<string, unknown> {
25
+ return (namedStep(stepName).try as { args: { body: Record<string, unknown> } }).args.body;
39
26
  }
40
27
 
41
- function requestAuth(stepName: string): Record<string, unknown> {
42
- const step = namedStep(stepName);
43
- const attempt = step.try as {
44
- args: {
45
- auth: Record<string, unknown>;
46
- };
47
- };
48
- return attempt.args.auth;
28
+ function chunkRequestBody(): Record<string, unknown> {
29
+ const renderChunks = namedStep("renderChunks").parallel as { for: { steps: Step[] } };
30
+ const render = namedStep("renderOneChunk", renderChunks.for.steps);
31
+ return (render.try as { args: { body: Record<string, unknown> } }).args.body;
49
32
  }
50
33
 
51
- function chunkRequestBody(stepName: string): Record<string, unknown> {
52
- const renderChunks = namedStep("renderChunks");
53
- const parallel = renderChunks.parallel as {
54
- for: {
55
- steps: Step[];
56
- };
57
- };
58
- const step = namedStep(stepName, parallel.for.steps);
59
- const attempt = step.try as {
60
- args: {
61
- body: Record<string, unknown>;
62
- };
63
- };
64
- return attempt.args.body;
65
- }
66
-
67
- describe("Cloud Workflows plan protocol routing", () => {
68
- it("pins OIDC tokens to the Cloud Run root and retries IAM propagation", () => {
69
- for (const stepName of ["planV1", "planV2", "assembleV1", "assembleV2"]) {
70
- expect(requestAuth(stepName)).toEqual({
71
- type: "OIDC",
72
- audience: "${serviceUrl}",
73
- });
74
- }
75
- expect(JSON.stringify(workflow.retryable)).toContain("e.code == 403");
76
- expect(source.match(/max_retries: 6/g)).toHaveLength(2);
77
- expect(source.match(/max_retries: 4/g)).toHaveLength(4);
78
- });
79
-
80
- it("defaults omitted protocol to v2 and rejects unknown protocols before plan", () => {
34
+ describe("Cloud Workflows Plan v2 orchestration", () => {
35
+ it("defaults an omitted protocol to v2 and rejects every explicit non-v2 value", () => {
81
36
  expect(source).toContain('default(map.get(args, "PlanProtocol"), "v2")');
82
- expect(namedStep("selectPlanProtocol")).toMatchObject({
37
+ expect(namedStep("checkPlanProtocol")).toMatchObject({
38
+ switch: [{ condition: '${planProtocol == "v2"}', next: "plan" }],
83
39
  next: "unsupportedPlanProtocol",
84
40
  });
85
41
  expect(namedStep("unsupportedPlanProtocol")).toMatchObject({
86
- raise: {
87
- code: "PLAN_PROTOCOL_UNSUPPORTED",
88
- },
42
+ raise: { code: "PLAN_PROTOCOL_UNSUPPORTED" },
89
43
  });
90
44
  });
91
45
 
92
- it("uses disjoint v1 and v2 plan request/response contracts", () => {
93
- expect(requestBody("planV1")).toMatchObject({
94
- Action: "plan",
95
- PlanProtocol: "v1",
96
- });
97
- expect(requestBody("planV2")).toMatchObject({
98
- Action: "plan",
99
- PlanProtocol: "v2",
100
- });
46
+ it("uses Plan v2 for every distributed action", () => {
47
+ for (const body of [requestBody("plan"), chunkRequestBody(), requestBody("assemble")]) {
48
+ expect(body.PlanProtocol).toBe("v2");
49
+ expect(body).not.toHaveProperty("PlanGcsUri");
50
+ }
51
+ expect(chunkRequestBody()).toHaveProperty("PlanV2ManifestGcsUri");
52
+ expect(chunkRequestBody()).toHaveProperty("PlanV2ArtifactGcsPrefix");
53
+ expect(requestBody("assemble")).toMatchObject({ AudioGcsUri: null });
54
+ expect(source).not.toContain("PlanProtocol: v1");
55
+ });
56
+
57
+ it("validates the v2 plan response before fan-out", () => {
101
58
  const validation = JSON.stringify(namedStep("validatePlanResult"));
102
- expect(validation).toContain("PlanGcsUri");
103
59
  expect(validation).toContain("PlanV2ManifestGcsUri");
104
60
  expect(validation).toContain("PlanV2ArtifactGcsPrefix");
105
- expect(source).toContain('not("PlanGcsUri" in planResult)');
106
- expect(source).toContain(
107
- '(not("PlanProtocol" in planResult) or planResult.PlanProtocol == "v1")',
108
- );
109
- });
110
-
111
- it("never mixes v1 and v2 chunk locators", () => {
112
- const v1 = chunkRequestBody("renderOneChunkV1");
113
- expect(v1).toMatchObject({
114
- Action: "renderChunk",
115
- PlanProtocol: "v1",
116
- });
117
- expect(v1).toHaveProperty("PlanGcsUri");
118
- expect(v1).not.toHaveProperty("PlanV2ManifestGcsUri");
119
- expect(v1).not.toHaveProperty("PlanV2ArtifactGcsPrefix");
120
-
121
- const v2 = chunkRequestBody("renderOneChunkV2");
122
- expect(v2).toMatchObject({
123
- Action: "renderChunk",
124
- PlanProtocol: "v2",
61
+ expect(validation).toContain("PlanGcsUri");
62
+ expect(validation).toContain("chunkCount > 0");
63
+ expect(namedStep("invalidPlanResult")).toMatchObject({
64
+ raise: { code: "PLAN_RESULT_INVALID" },
125
65
  });
126
- expect(v2).not.toHaveProperty("PlanGcsUri");
127
- expect(v2).toHaveProperty("PlanV2ManifestGcsUri");
128
- expect(v2).toHaveProperty("PlanV2ArtifactGcsPrefix");
129
- expect(v2).toHaveProperty("PlanHash");
130
66
  });
131
67
 
132
- it("never mixes v1 and v2 assembler locators", () => {
133
- const v1 = requestBody("assembleV1");
134
- expect(v1).toHaveProperty("PlanGcsUri");
135
- expect(v1).not.toHaveProperty("PlanV2ManifestGcsUri");
136
- expect(v1).not.toHaveProperty("PlanV2ArtifactGcsPrefix");
137
-
138
- const v2 = requestBody("assembleV2");
139
- expect(v2).not.toHaveProperty("PlanGcsUri");
140
- expect(v2).toHaveProperty("PlanV2ManifestGcsUri");
141
- expect(v2).toHaveProperty("PlanV2ArtifactGcsPrefix");
142
- expect(v2).toHaveProperty("PlanHash");
143
- expect(v2).toMatchObject({
144
- PlanProtocol: "v2",
145
- AudioGcsUri: null,
146
- });
68
+ it("pins OIDC tokens to the service and retries transient failures", () => {
69
+ expect(source.match(/audience: \$\{serviceUrl\}/g)).toHaveLength(3);
70
+ expect(JSON.stringify(workflow.retryable)).toContain("e.code == 403");
71
+ expect(source.match(/max_retries: 6/g)).toHaveLength(1);
72
+ expect(source.match(/max_retries: 4/g)).toHaveLength(2);
147
73
  });
148
74
  });