[verified] feat: add safe source importers

This commit is contained in:
2026-09-03 19:11:51 +00:00
parent 5146b14d43
commit bb378ebd63
4 changed files with 1136 additions and 6 deletions
@@ -0,0 +1,385 @@
import assert from "node:assert/strict";
import { mkdtemp, mkdir, symlink, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import test from "node:test";
import { importSource } from "./index.js";
import { validateDiagramIR } from "../../model/diagram-ir.js";
const textInput = (sourceKind: Parameters<typeof importSource>[0]["sourceKind"], path: string, text: string) =>
importSource({ sourceKind, path, input: { type: "text", text } });
test("imports Python modules with exact line provenance", async () => {
const result = await textInput("python", "src/app.py", "import os\nfrom pkg.db import Client\n");
assert.deepEqual(result.diagram.pages[0].nodes.map((n) => [n.label, n.kind]), [
["src/app.py", "module"], ["os", "library"], ["pkg.db", "library"],
]);
assert.equal(result.diagram.pages[0].edges[1].provenance?.line, 2);
assert.equal(validateDiagramIR(result.diagram), result.diagram);
});
test("imports JavaScript and TypeScript static and require dependencies", async () => {
const js = await textInput("javascript", "src/app.js", "import express from 'express';\nconst x = require('./local.js');\n");
const ts = await textInput("typescript", "src/app.ts", "export { x } from '@scope/pkg';\nimport type { T } from './types.js';\n");
assert.deepEqual(js.diagram.pages[0].nodes.slice(1).map((n) => n.label), ["express", "./local.js"]);
assert.deepEqual(ts.diagram.pages[0].nodes.slice(1).map((n) => n.label), ["@scope/pkg", "./types.js"]);
assert.ok(js.diagram.pages[0].nodes.every((n) => ["module", "library"].includes(n.kind ?? "")));
});
test("JavaScript scanner ignores comments and strings while preserving real static imports", async () => {
const source = [
"// import commented from 'commented';",
"/* require('blocked');",
" export { x } from 'also-blocked'; */",
"const text = \"import fake from 'inside-string'\";",
"const template = `require('inside-template')`;",
"const pattern = /import regexFake from 'inside-regex'/;",
"loader.require('member-call');",
"const metadata = { import: 'property', from: 'property-from' };",
"import real from 'real';",
"export { thing } from './actual.js';",
"const required = require(\"required\");",
"import type {",
" Value",
"} from './types.js';",
"// const dynamic = import(name);",
].join("\n");
const result = await textInput("typescript", "src/app.ts", source);
assert.deepEqual(result.diagram.pages[0].nodes.slice(1).map((node) => node.label), ["real", "./actual.js", "required", "./types.js"]);
assert.deepEqual(result.diagnostics, []);
});
test("JavaScript scanner treats arrow-expression regex literals as inert without hiding division or imports", async () => {
const source = [
"const matcher = () => /require('phantom')/;",
"const ratio = numerator / denominator;",
"import real from 'real';",
"const actual = require('./actual.js');",
].join("\n");
const result = await textInput("typescript", "src/regex.ts", source);
assert.deepEqual(result.diagram.pages[0].nodes.slice(1).map((node) => node.label), ["real", "./actual.js"]);
assert.deepEqual(result.diagnostics, []);
});
test("JavaScript scanner diagnoses unsupported dynamic and unterminated constructs", async () => {
const result = await textInput("javascript", "src/app.js", "const dynamic = import(name);\nconst template = `${load(name)}`;\n/* unterminated");
assert.ok(result.diagnostics.length >= 3);
assert.ok(result.diagnostics.every((diagnostic) => diagnostic.code === "unknown-construct"));
});
test("imports Go single and grouped imports", async () => {
const result = await textInput("go", "cmd/main.go", "package main\nimport \"fmt\"\nimport (\n alias \"example.com/acme/lib\"\n _ \"net/http/pprof\"\n)\n");
assert.deepEqual(result.diagram.pages[0].nodes.slice(1).map((n) => n.label), ["fmt", "example.com/acme/lib", "net/http/pprof"]);
assert.equal(result.diagram.pages[0].edges[2].provenance?.line, 5);
});
test("imports Rust mod declarations and use roots without guessing macros", async () => {
const result = await textInput("rust", "src/lib.rs", "mod api;\nuse crate::db::Pool;\nuse serde::{Serialize, Deserialize};\ncustom!(unknown);\n");
assert.deepEqual(result.diagram.pages[0].nodes.slice(1).map((n) => n.label), ["api", "crate::db", "serde"]);
assert.equal(result.diagnostics[0].code, "unknown-construct");
});
test("imports Terraform resources and explicit references", async () => {
const result = await textInput("terraform", "main.tf", "resource \"aws_vpc\" \"main\" {}\nresource \"aws_subnet\" \"web\" {\n vpc_id = aws_vpc.main.id\n}\n");
assert.deepEqual(result.diagram.pages[0].nodes.map((n) => n.label), ["aws_subnet.web", "aws_vpc.main"]);
assert.deepEqual(result.diagram.pages[0].edges.map((e) => [e.source, e.target, e.kind]), [["resource-6177735f7375626e65742e776562", "resource-6177735f7670632e6d61696e", "reference"]]);
assert.equal(result.diagram.pages[0].edges[0].provenance?.line, 3);
});
test("non-JavaScript scanners ignore inert constructs and diagnose unterminated regions", async () => {
const [python, unterminatedPython, go, rust, sql, terraform] = await Promise.all([
textInput("python", "app.py", "'''\nimport phantom\n'''\nimport real\n"),
textInput("python", "broken.py", "\"\"\"unterminated\nimport phantom\n"),
textInput("go", "main.go", "package main\n/*\nimport \"phantom\"\n*/\nimport \"fmt\"\n/* unterminated\n"),
textInput("rust", "lib.rs", "/*\nmod phantom;\nuse fake::Thing;\n*/\nmod api;\nuse serde::Serialize;\n/* unterminated\n"),
textInput("sql", "schema.sql", "/*\nCREATE TABLE phantom (id INT REFERENCES ghost(id));\n*/\nCREATE TABLE real (id INT);\n/* unterminated\n"),
textInput("terraform", "main.tf", [
"/*", "resource \"aws_vpc\" \"phantom\" {}", "*/",
"resource \"aws_vpc\" \"main\" {}",
"resource \"aws_subnet\" \"web\" {",
" description = \"aws_vpc.phantom.id\"",
" # fake = aws_vpc.phantom.id",
" vpc_id = aws_vpc.main.id",
"}", "/* unterminated",
].join("\n")),
]);
assert.deepEqual(python.diagram.pages[0].nodes.map((node) => node.label), ["app.py", "real"]);
assert.deepEqual(unterminatedPython.diagram.pages[0].nodes.map((node) => node.label), ["broken.py"]);
assert.deepEqual(go.diagram.pages[0].nodes.map((node) => node.label), ["main.go", "fmt"]);
assert.deepEqual(rust.diagram.pages[0].nodes.map((node) => node.label), ["lib.rs", "api", "serde"]);
assert.deepEqual(sql.diagram.pages[0].nodes.map((node) => node.label), ["real"]);
assert.deepEqual(terraform.diagram.pages[0].nodes.map((node) => node.label), ["aws_subnet.web", "aws_vpc.main"]);
assert.equal(terraform.diagram.pages[0].edges.length, 1);
for (const result of [unterminatedPython, go, rust, sql, terraform]) {
assert.ok(result.diagnostics.some((diagnostic) => diagnostic.code === "unknown-construct"));
}
});
test("multiline literals mask phantom topology and diagnose only unterminated regions", async () => {
const [go, rust, sql, terraform] = await Promise.all([
textInput("go", "main.go", [
"package main", "var first = `", "import \"phantom\"", "`", "import \"fmt\"",
].join("\n")),
textInput("rust", "lib.rs", [
"const FIRST: &str = r\"", "mod phantom_one;", "\";",
"const SECOND: &str = r##\"", "use phantom_two::Thing;", "\"##;",
"mod api;", "use serde::Serialize;",
].join("\n")),
textInput("sql", "schema.sql", [
"CREATE TABLE real (id INT);", "INSERT INTO real(note) VALUES ('",
"CREATE TABLE phantom (id INT REFERENCES ghost(id));", "escaped '' quote", "');",
].join("\n")),
textInput("terraform", "main.tf", [
"resource \"aws_vpc\" \"main\" {}", "resource \"aws_subnet\" \"web\" {",
" user_data = <<-EOT", " resource \"aws_instance\" \"phantom\" {}",
" aws_vpc.phantom.id", " EOT", " vpc_id = aws_vpc.main.id", "}",
].join("\n")),
]);
assert.deepEqual(go.diagram.pages[0].nodes.map((node) => node.label), ["main.go", "fmt"]);
assert.deepEqual(rust.diagram.pages[0].nodes.map((node) => node.label), ["lib.rs", "api", "serde"]);
assert.deepEqual(sql.diagram.pages[0].nodes.map((node) => node.label), ["real"]);
assert.deepEqual(terraform.diagram.pages[0].nodes.map((node) => node.label), ["aws_subnet.web", "aws_vpc.main"]);
assert.equal(terraform.diagram.pages[0].edges.length, 1);
for (const result of [go, rust, sql, terraform]) assert.deepEqual(result.diagnostics, []);
const unterminated = await Promise.all([
textInput("go", "broken.go", "package main\nvar value = `unterminated\nimport \"phantom\""),
textInput("rust", "broken.rs", "const VALUE: &str = r###\"unterminated\nmod phantom;"),
textInput("sql", "broken.sql", "CREATE TABLE real (id INT);\nINSERT INTO real(note) VALUES ('unterminated\nCREATE TABLE phantom (id INT);"),
textInput("terraform", "broken.tf", "resource \"aws_vpc\" \"main\" {}\nvalue = <<EOF\naws_vpc.phantom.id"),
]);
for (const result of unterminated) {
assert.equal(result.diagram.pages[0].nodes.some((node) => node.label.includes("phantom")), false);
assert.deepEqual(result.diagnostics.map((diagnostic) => diagnostic.code), ["unknown-construct"]);
}
});
test("imports Kubernetes relationships and redacts Secret payloads", async () => {
const objects = [
{ apiVersion: "v1", kind: "Secret", metadata: { name: "db-secret" }, data: { password: "c2VjcmV0" }, stringData: { token: "plain" } },
{ apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "api" }, spec: { selector: { matchLabels: { app: "api" } }, template: { metadata: { labels: { app: "api" } }, spec: { containers: [{ envFrom: [{ secretRef: { name: "db-secret" } }] }] } } } },
{ apiVersion: "v1", kind: "Service", metadata: { name: "api-svc" }, spec: { selector: { app: "api" } } },
];
const result = await importSource({ sourceKind: "kubernetes", path: "k8s.json", input: { type: "object", value: objects } });
assert.deepEqual(result.diagram.pages[0].nodes.map((n) => n.label), ["Deployment/default/api", "Secret/default/db-secret", "Service/default/api-svc"]);
assert.deepEqual(result.diagram.pages[0].edges.map((e) => e.kind), ["uses-secret", "selects"]);
assert.equal(JSON.stringify(result).includes("c2VjcmV0"), false);
assert.equal(JSON.stringify(result).includes("plain"), false);
assert.equal(result.diagnostics[0].code, "secret-redacted");
});
test("Kubernetes ignores secretRef-shaped data outside supported workload fields", async () => {
const objects = [
{ kind: "Secret", metadata: { name: "real" } },
{ kind: "Secret", metadata: { name: "phantom" } },
{
kind: "Deployment", metadata: { name: "api" }, status: { secretRef: { name: "phantom" } },
spec: { template: { spec: { containers: [{ envFrom: [{ secretRef: { name: "real" } }] }] } } },
},
];
const result = await importSource({ sourceKind: "kubernetes", path: "k8s.json", input: { type: "object", value: objects } });
const labelById = new Map(result.diagram.pages[0].nodes.map((node) => [node.id, node.label]));
assert.deepEqual(result.diagram.pages[0].edges.map((edge) => [edge.kind, labelById.get(edge.target)]), [
["uses-secret", "Secret/default/real"],
]);
});
test("scopes Kubernetes identity, secret references, and non-empty Service selectors by namespace", async () => {
const objects = [
{ kind: "Secret", metadata: { name: "shared" } },
{ kind: "Secret", metadata: { namespace: "team-b", name: "shared" } },
{ kind: "Deployment", metadata: { name: "api" }, spec: { selector: { matchLabels: { selectorOnly: "wrong" } }, template: { metadata: { labels: { app: "api" } }, spec: { containers: [{ envFrom: [{ secretRef: { name: "shared" } }] }] } } } },
{ kind: "Deployment", metadata: { namespace: "team-b", name: "api" }, spec: { template: { metadata: { labels: { app: "api" } }, spec: { containers: [{ envFrom: [{ secretRef: { name: "shared" } }] }] } } } },
{ kind: "Deployment", metadata: { name: "selector-only" }, spec: { selector: { matchLabels: { app: "api" } }, template: { metadata: { labels: { app: "other" } } } } },
{ kind: "Service", metadata: { name: "api" }, spec: { selector: { app: "api" } } },
{ kind: "Service", metadata: { name: "empty" }, spec: { selector: {} } },
];
const result = await importSource({ sourceKind: "kubernetes", path: "k8s.json", input: { type: "object", value: objects } });
const nodes = result.diagram.pages[0].nodes;
assert.equal(new Set(nodes.map((node) => node.id)).size, nodes.length);
assert.ok(nodes.some((node) => node.label === "Secret/default/shared"));
assert.ok(nodes.some((node) => node.label === "Secret/team-b/shared"));
const labelById = new Map(nodes.map((node) => [node.id, node.label]));
const relationships = result.diagram.pages[0].edges.map((edge) => [labelById.get(edge.source), edge.kind, labelById.get(edge.target)]);
assert.deepEqual(relationships, [
["Deployment/default/api", "uses-secret", "Secret/default/shared"],
["Deployment/team-b/api", "uses-secret", "Secret/team-b/shared"],
["Service/default/api", "selects", "Deployment/default/api"],
]);
});
test("imports Docker Compose service dependencies and named volumes", async () => {
const result = await importSource({ sourceKind: "docker-compose", path: "compose.json", input: { type: "object", value: {
services: { api: { depends_on: ["db"], volumes: ["data:/var/lib/app"] }, db: { image: "postgres" } }, volumes: { data: {} },
} } });
assert.deepEqual(result.diagram.pages[0].nodes.map((n) => [n.label, n.kind]), [["api", "service"], ["db", "service"], ["data", "volume"]]);
assert.deepEqual(result.diagram.pages[0].edges.map((e) => e.kind), ["depends-on", "mounts"]);
});
test("imports SQL tables and foreign keys", async () => {
const result = await textInput("sql", "schema.sql", "CREATE TABLE users (id INT PRIMARY KEY);\nCREATE TABLE orders (\n user_id INT,\n FOREIGN KEY (user_id) REFERENCES users(id)\n);\n");
assert.deepEqual(result.diagram.pages[0].nodes.map((n) => n.label), ["orders", "users"]);
assert.deepEqual(result.diagram.pages[0].edges.map((e) => [e.source, e.target, e.kind]), [["table-6f7264657273", "table-7573657273", "foreign-key"]]);
assert.equal(result.diagram.pages[0].edges[0].provenance?.line, 4);
});
test("orders importer output by Unicode code point without locale-sensitive comparison", async () => {
const sql = await textInput("sql", "schema.sql", "CREATE TABLE a (id INT);\nCREATE TABLE Z (id INT);\n");
assert.deepEqual(sql.diagram.pages[0].nodes.map((node) => node.label), ["Z", "a"]);
const kubernetes = await importSource({ sourceKind: "kubernetes", path: "k8s.json", input: { type: "object", value: [
{ kind: "Service", metadata: { name: "a" }, spec: { selector: {} } },
{ kind: "Service", metadata: { name: "Z" }, spec: { selector: {} } },
{ kind: "Service", metadata: { name: "😀" }, spec: { selector: {} } },
{ kind: "Service", metadata: { name: "\uE000" }, spec: { selector: {} } },
] } });
assert.deepEqual(kubernetes.diagram.pages[0].nodes.map((node) => node.label), ["Service/default/Z", "Service/default/a", "Service/default/\uE000", "Service/default/😀"]);
});
test("imports OpenAPI operations and schema references", async () => {
const result = await importSource({ sourceKind: "openapi", path: "openapi.json", input: { type: "object", value: {
openapi: "3.1.0", paths: { "/pets": { get: { operationId: "listPets", responses: { "200": { content: { "application/json": { schema: { $ref: "#/components/schemas/Pet" } } } } } } } },
components: { schemas: { Pet: { type: "object" } } },
} } });
assert.deepEqual(result.diagram.pages[0].nodes.map((n) => [n.label, n.kind]), [["GET /pets", "command"], ["Pet", "library"]]);
assert.equal(result.diagram.pages[0].edges[0].kind, "schema-reference");
});
test("OpenAPI inherited schema names remain unknown references", async () => {
const result = await importSource({ sourceKind: "openapi", path: "openapi.json", input: { type: "object", value: {
paths: { "/unsafe": { get: { responses: { "200": { content: { "application/json": { schema: { $ref: "#/components/schemas/toString" } } } } } } } },
components: { schemas: {} },
} } });
assert.deepEqual(result.diagram.pages[0].nodes.map((node) => node.label), ["GET /unsafe"]);
assert.deepEqual(result.diagram.pages[0].edges, []);
assert.deepEqual(result.diagnostics.map((diagnostic) => diagnostic.code), ["unknown-reference"]);
});
test("imports CI job DAG needs", async () => {
const result = await importSource({ sourceKind: "ci", path: ".github/workflows/ci.json", input: { type: "object", value: {
jobs: { deploy: { needs: ["build", "test"] }, test: { needs: "build" }, build: { "runs-on": "ubuntu-latest" } },
} } });
assert.deepEqual(result.diagram.pages[0].nodes.map((n) => [n.label, n.kind]), [["build", "command"], ["deploy", "command"], ["test", "command"]]);
assert.deepEqual(result.diagram.pages[0].edges.map((e) => [e.source, e.target]), [["command-6275696c64", "command-6465706c6f79"], ["command-74657374", "command-6465706c6f79"], ["command-6275696c64", "command-74657374"]]);
});
test("keeps generated IDs unique for normalized names, ambiguous composites, and repeated relationships", async () => {
const result = await importSource({ sourceKind: "ci", path: "ci.json", input: { type: "object", value: {
jobs: {
A: {},
"A": {},
a: {},
"a-needs-b": {},
"b-needs-c": { needs: ["a", "a"] },
c: { needs: "a-needs-b" },
},
} } });
const nodes = result.diagram.pages[0].nodes;
const edges = result.diagram.pages[0].edges;
assert.equal(new Set(nodes.map((node) => node.id)).size, nodes.length);
assert.equal(new Set(edges.map((edge) => edge.id)).size, edges.length);
assert.equal(edges.length, 3);
assert.doesNotThrow(() => validateDiagramIR(result.diagram));
});
test("keeps sanitized IDs unique and reports duplicate logical definitions", async () => {
const openapi = await importSource({ sourceKind: "openapi", path: "api.json", input: { type: "object", value: { paths: { "/x": { get: {} } }, components: { schemas: { "A/B": {}, "A-B": {} } } } } });
assert.equal(new Set(openapi.diagram.pages[0].nodes.map((n) => n.id)).size, openapi.diagram.pages[0].nodes.length);
const sql = await textInput("sql", "schema.sql", "CREATE TABLE users (id INT);\nCREATE TABLE users (id INT);\n");
assert.equal(sql.diagnostics[0].code, "duplicate-id");
assert.doesNotThrow(() => validateDiagramIR(openapi.diagram));
});
test("disambiguates every repeated relationship occurrence deterministically", async () => {
const sqlText = "CREATE TABLE users (id INT PRIMARY KEY);\nCREATE TABLE orders (\n a INT REFERENCES users(id),\n b INT REFERENCES users(id)\n);\n";
const terraformText = "resource \"aws_vpc\" \"main\" {}\nresource \"aws_subnet\" \"web\" {\n first = aws_vpc.main.id\n second = aws_vpc.main.id\n}\n";
const composeValue = { services: { api: { depends_on: ["db", "db"], volumes: ["data:/one", "data:/two"] }, db: {} }, volumes: { data: {} } };
const results = await Promise.all([
textInput("sql", "schema.sql", sqlText),
textInput("terraform", "main.tf", terraformText),
importSource({ sourceKind: "docker-compose", path: "compose.json", input: { type: "object", value: composeValue } }),
]);
const expectedEdgeCounts = [2, 2, 4];
results.forEach((result, index) => {
const edges = result.diagram.pages[0].edges;
assert.equal(edges.length, expectedEdgeCounts[index]);
assert.equal(new Set(edges.map((edge) => edge.id)).size, edges.length);
assert.doesNotThrow(() => validateDiagramIR(result.diagram));
});
const again = await textInput("terraform", "main.tf", terraformText);
assert.deepEqual(again.diagram.pages[0].edges.map((edge) => edge.id), results[1].diagram.pages[0].edges.map((edge) => edge.id));
});
test("rejects oversized and malformed inputs without executing object values", async () => {
await assert.rejects(() => importSource({ sourceKind: "python", path: "x.py", maxBytes: 4, input: { type: "text", text: "import os" } }), /exceeds.*4/i);
await assert.rejects(() => importSource({ sourceKind: "ci", path: "ci.json", input: { type: "object", value: { jobs: null } } }), /malformed CI/i);
let accessed = false;
const tagged = Object.create({ yamlTag: "!exec" }) as Record<string, unknown>;
Object.defineProperty(tagged, "jobs", { enumerable: true, get() { accessed = true; return {}; } });
await assert.rejects(() => importSource({ sourceKind: "ci", path: "ci.json", input: { type: "object", value: tagged } }), /plain JSON/i);
assert.equal(accessed, false);
});
test("rejects object input beyond the conservative nesting bound without stack overflow", async () => {
const root: Record<string, unknown> = {};
let cursor = root;
for (let depth = 0; depth < 10_000; depth += 1) {
const child: Record<string, unknown> = {};
cursor.child = child;
cursor = child;
}
await assert.rejects(
() => importSource({ sourceKind: "ci", path: "ci.json", input: { type: "object", value: root } }),
/nesting.*128/i,
);
});
test("rejects file traversal and symlink escapes from the declared root", async () => {
const base = await mkdtemp(join(tmpdir(), "source-import-"));
const root = join(base, "root"); await mkdir(root);
await writeFile(join(base, "outside.py"), "import os\n");
await symlink(join(base, "outside.py"), join(root, "escape.py"));
await assert.rejects(() => importSource({ sourceKind: "python", path: "../outside.py", input: { type: "file", root } }), /outside.*root|escape/i);
await assert.rejects(() => importSource({ sourceKind: "python", path: "escape.py", input: { type: "file", root } }), /outside.*root|symlink.*escape/i);
});
test("refuses even root-confined symlinks so substitution cannot change an opened source", async () => {
const root = await mkdtemp(join(tmpdir(), "source-no-follow-"));
await writeFile(join(root, "real.py"), "import safe\n");
await symlink("real.py", join(root, "substituted.py"));
await assert.rejects(
() => importSource({ sourceKind: "python", path: "substituted.py", input: { type: "file", root } }),
/symlink|symbolic|safe source/i,
);
});
test("ingests directories in deterministic path order with confined provenance", async () => {
const root = await mkdtemp(join(tmpdir(), "source-directory-"));
await mkdir(join(root, "src"));
await writeFile(join(root, "src", "b.py"), "import zlib\n");
await writeFile(join(root, "src", "a.py"), "from pkg import x\n");
const request = { sourceKind: "python" as const, path: "src", input: { type: "file" as const, root } };
const first = await importSource(request); const second = await importSource(request);
assert.equal(JSON.stringify(first), JSON.stringify(second));
assert.deepEqual(first.diagram.pages[0].nodes.filter((n) => n.kind === "module").map((n) => n.label), ["src/a.py", "src/b.py"]);
assert.deepEqual(first.diagram.pages[0].nodes.filter((n) => n.kind === "library").map((n) => n.label), ["pkg", "zlib"]);
assert.equal(first.diagram.pages[0].edges[0].provenance?.path, "src/a.py");
assert.doesNotThrow(() => validateDiagramIR(first.diagram));
});
test("reports dynamic code imports as unknown constructs instead of guessing", async () => {
const python = await textInput("python", "dynamic.py", "module = __import__(name)\n");
const javascript = await textInput("javascript", "dynamic.js", "const module = await import(name);\n");
assert.equal(python.diagnostics[0].code, "unknown-construct");
assert.equal(javascript.diagnostics[0].code, "unknown-construct");
assert.deepEqual(python.diagram.pages[0].nodes.map((n) => n.label), ["dynamic.py"]);
});
test("loads YAML data without permitting custom tags", async () => {
const root = await mkdtemp(join(tmpdir(), "source-yaml-"));
await writeFile(join(root, "objects.yaml"), "apiVersion: v1\nkind: Service\nmetadata:\n name: api\nspec:\n selector:\n app: api\n");
await writeFile(join(root, "tagged.yaml"), "jobs: !exec dangerous\n");
const result = await importSource({ sourceKind: "kubernetes", path: "objects.yaml", input: { type: "file", root } });
assert.deepEqual(result.diagram.pages[0].nodes.map((n) => n.label), ["Service/default/api"]);
await assert.rejects(() => importSource({ sourceKind: "ci", path: "tagged.yaml", input: { type: "file", root } }), /tag|malformed YAML/i);
});