Files
diagrams-drawio/scripts/src/services/source-importers/index.test.ts
T

386 lines
24 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
});