mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-10 22:20:14 +00:00
fix(core,cli): parse figma 403 body, batch asset fetch, fix NO_TOKEN box
Extends the scope+retry work from the figma bug-bash (valid report:
9-bugs-with-repros; the skill-not-used report was discarded).
- 403-body parse (bug 4): figma returns 403 {"err":"Invalid token"} for bad
PATs (NOT 401), and 403 {"err":"Invalid scope(s)… requires X"} for missing
scopes. get() now reads the body: "Invalid token" reclassifies to BAD_TOKEN
with re-mint advice; a scope body surfaces figma's own diagnosis verbatim;
else falls back to the endpoint's scope hint. Reads both err and message
(variables endpoint uses message). One fix, honest messages for bugs 1/4/9.
- Batch asset fetch (requested): figma asset accepts multiple refs
(space-separated or comma-joined) of one file and renders them in a SINGLE
/v1/images call via new client.renderNodes — figma's documented per-minute
rate-limit workaround. runAssetImport delegates to runAssetImportMany;
cache-checks per node, batches only the misses, one index.md regen.
- NO_TOKEN box (bug 8): errorBox indented only the first hint line, mangling
the numbered setup list. Indent every line; single-line hints unchanged.
Verified live: 3 refs -> 3 imports -> 1 request; bad token -> BAD_TOKEN not
scope advice. Client suite 22, cli figma 33.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
1bb7688347
commit
4fc699fee6
@@ -3,7 +3,7 @@ import { describe, expect, it, afterEach } from "vitest";
|
||||
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { runAssetImport, type AssetImportDeps } from "./asset.js";
|
||||
import { runAssetImport, runAssetImportMany, type AssetImportDeps } from "./asset.js";
|
||||
import type { FigmaClient } from "@hyperframes/core/figma";
|
||||
|
||||
const dirs: string[] = [];
|
||||
@@ -17,8 +17,9 @@ afterEach(() => {
|
||||
});
|
||||
|
||||
function fakeClient(overrides: Partial<FigmaClient> = {}): FigmaClient {
|
||||
return {
|
||||
const client: FigmaClient = {
|
||||
renderNode: () => Promise.resolve({ url: "https://cdn.example/a", ext: "png" }),
|
||||
renderNodes: () => Promise.resolve([]),
|
||||
imageFills: () => Promise.resolve(new Map()),
|
||||
variables: () => Promise.resolve({ variables: {}, variableCollections: {} }),
|
||||
styles: () => Promise.resolve([]),
|
||||
@@ -26,6 +27,19 @@ function fakeClient(overrides: Partial<FigmaClient> = {}): FigmaClient {
|
||||
fileVersion: () => Promise.resolve({ version: "7", lastModified: "2026-07-01" }),
|
||||
...overrides,
|
||||
};
|
||||
// Default renderNodes delegates to renderNode (honoring any override) so
|
||||
// existing single-node tests keep controlling behavior via renderNode.
|
||||
if (!overrides.renderNodes) {
|
||||
client.renderNodes = (fileKey, nodeIds, opts) =>
|
||||
Promise.all(
|
||||
nodeIds.map((nodeId) =>
|
||||
client
|
||||
.renderNode({ fileKey, nodeId }, opts)
|
||||
.then((r) => ({ nodeId, url: r.url, ext: r.ext })),
|
||||
),
|
||||
);
|
||||
}
|
||||
return client;
|
||||
}
|
||||
|
||||
const PNG_BYTES = new Uint8Array([0x89, 0x50, 0x4e, 0x47]);
|
||||
@@ -134,6 +148,39 @@ describe("runAssetImport", () => {
|
||||
expect(index).not.toContain("image_002");
|
||||
});
|
||||
|
||||
it("batches many nodes into ONE renderNodes call and freezes each", async () => {
|
||||
const dir = scratch();
|
||||
let renderNodesCalls = 0;
|
||||
let batchSize = 0;
|
||||
const batchClient = fakeClient({
|
||||
renderNodes: (fileKey, nodeIds, opts) => {
|
||||
renderNodesCalls += 1;
|
||||
batchSize = nodeIds.length;
|
||||
return Promise.resolve(
|
||||
nodeIds.map((nodeId) => ({ nodeId, url: `https://cdn/${nodeId}`, ext: opts.format })),
|
||||
);
|
||||
},
|
||||
});
|
||||
const results = await runAssetImportMany(
|
||||
["KEY:1-2", "KEY:3-4", "KEY:5-6"],
|
||||
{ format: "png" },
|
||||
deps(dir, { client: batchClient }),
|
||||
);
|
||||
expect(results).toHaveLength(3);
|
||||
expect(results.every((r) => !r.reused)).toBe(true);
|
||||
expect(renderNodesCalls).toBe(1); // one REST call for all three
|
||||
expect(batchSize).toBe(3);
|
||||
// distinct frozen files, all recorded
|
||||
expect(new Set(results.map((r) => r.record.id)).size).toBe(3);
|
||||
});
|
||||
|
||||
it("splits comma-joined refs and rejects a cross-file batch", async () => {
|
||||
const dir = scratch();
|
||||
await expect(
|
||||
runAssetImportMany(["KEY:1-2", "OTHER:3-4"], { format: "png" }, deps(dir)),
|
||||
).rejects.toThrow(/share a fileKey/);
|
||||
});
|
||||
|
||||
it("reuses against ANY matching tuple, not just the oldest row", async () => {
|
||||
const dir = scratch();
|
||||
await runAssetImport("KEY:1-2", { format: "svg" }, deps(dir)); // image_001 (svg)
|
||||
|
||||
Reference in New Issue
Block a user