mirror of
https://github.com/penpot/penpot.git
synced 2026-11-01 15:40:41 +00:00
Add a main RPC command, get-semantic-file, that turns a Penpot file into a small, neutral projection: pages, frames, text, links and interactions, with libraries, geometry and assets stripped. The projection is what a decision model sees, so the extractor stays model-agnostic and the deterministic checks live outside it. Add experiments/security-projection, a Node prototype that consumes the command: analyze.mjs (file -> projection -> checks -> decision model), compare.mjs (one file, several models), evaluate.mjs with a labeled set of projections, and seed.mjs, which builds real files to validate the extractor. It talks to OpenRouter decision models. Tests: backend_tests.rpc-semantic-test (3 tests, 21 assertions) and 24 Node tests across the prototype. AI-assisted-by: deepseek-v4.1-flash
74 lines
2.3 KiB
JavaScript
74 lines
2.3 KiB
JavaScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
import {
|
|
collectSubtree,
|
|
cycling,
|
|
hasComponentRef,
|
|
pickTemplateFrame,
|
|
rewriteParagraphs,
|
|
} from "./seed.mjs";
|
|
|
|
const ZERO = "00000000-0000-0000-0000-000000000000";
|
|
|
|
function fixture() {
|
|
return {
|
|
[ZERO]: { id: ZERO, type: "frame", frameId: ZERO, parentId: ZERO, shapes: ["f1", "f2"] },
|
|
f1: {
|
|
id: "f1", type: "frame", frameId: ZERO, parentId: ZERO, shapes: ["t1", "r1"],
|
|
},
|
|
t1: {
|
|
id: "t1", type: "text", frameId: "f1", parentId: "f1",
|
|
content: {
|
|
type: "root",
|
|
children: [{
|
|
type: "paragraph-set",
|
|
children: [{ type: "paragraph", children: [{ text: "old", fontSize: "16" }] }],
|
|
}],
|
|
},
|
|
},
|
|
r1: {
|
|
id: "r1", type: "rect", frameId: "f1", parentId: "f1",
|
|
interactions: [{ actionType: "open-url", url: "http://old.example" }],
|
|
},
|
|
f2: { id: "f2", type: "frame", frameId: ZERO, parentId: ZERO, shapes: ["c1"] },
|
|
c1: { id: "c1", type: "rect", frameId: "f2", parentId: "f2", componentId: "comp-1" },
|
|
};
|
|
}
|
|
|
|
test("collectSubtree returns the node and its descendants in tree order", () => {
|
|
assert.deepEqual(collectSubtree(fixture(), "f1"), ["f1", "t1", "r1"]);
|
|
});
|
|
|
|
test("hasComponentRef sees component instances anywhere in the subtree", () => {
|
|
const objects = fixture();
|
|
assert.equal(hasComponentRef(objects, "f1"), false);
|
|
assert.equal(hasComponentRef(objects, "f2"), true);
|
|
});
|
|
|
|
test("pickTemplateFrame skips frames with component refs", () => {
|
|
const frame = pickTemplateFrame(fixture());
|
|
assert.equal(frame.id, "f1");
|
|
});
|
|
|
|
test("pickTemplateFrame throws when no plain frame has text and a link", () => {
|
|
const objects = fixture();
|
|
delete objects.r1.interactions;
|
|
assert.throws(() => pickTemplateFrame(objects), /no plain frame/);
|
|
});
|
|
|
|
test("rewriteParagraphs replaces the text and keeps the span styles", () => {
|
|
const objects = fixture();
|
|
rewriteParagraphs(objects.t1.content, cycling(["hello"]));
|
|
const span = objects.t1.content.children[0].children[0].children[0];
|
|
assert.equal(span.text, "hello");
|
|
assert.equal(span.fontSize, "16");
|
|
});
|
|
|
|
test("cycling wraps around", () => {
|
|
const next = cycling(["a", "b"]);
|
|
assert.equal(next(), "a");
|
|
assert.equal(next(), "b");
|
|
assert.equal(next(), "a");
|
|
});
|