This is an automated email from the ASF dual-hosted git repository.

github-merge-queue[bot] pushed a commit to branch 
gh-readonly-queue/main/pr-6651-aec180ef0297b539834b3c2ea8fe03f4e7b720d4
in repository https://gitbox.apache.org/repos/asf/texera.git

commit bde8e88971c33b3e3948f96ef8d86e68b7713962
Author: Matthew B. <[email protected]>
AuthorDate: Mon Jul 20 15:27:53 2026 -0700

    test(frontend): add unit tests for datasetVersionFileTree (#6651)
    
    ### What changes were proposed in this PR?
    - Add `frontend/src/app/common/type/datasetVersionFileTree.spec.ts`, a
    new Vitest spec for the dataset version file tree helpers, which
    previously had no dedicated unit tests.
    - Cover getFullPathFromDatasetFileNode and
    getRelativePathFromDatasetFileNode (three-segment strip, empty-segment
    filtering, and the <=3 returns empty case).
    - Cover recursive getPathsUnderOrEqualDatasetFileNode for file,
    nested-dir, and empty-dir nodes.
    - Cover DatasetVersionFileTreeManager: nested build, add idempotency,
    siblings, and removal by identity and by path including subtree, absent,
    synthetic-root guard, and double-removal no-ops.
    ### Any related issues, documentation, discussions?
    Closes: #6650
    ### How was this PR tested?
    - Run: `cd frontend && node --max-old-space-size=8192
    ./node_modules/nx/dist/bin/nx.js test gui --watch=false
    --include=src/app/common/type/datasetVersionFileTree.spec.ts`, expect
    all 22 tests passing.
    - Test-only change; no production code is modified.
    ### Was this PR authored or co-authored using generative AI tooling?
    Co-authored with Claude Opus 4.8 in compliance with ASF
    
    ---------
    
    Signed-off-by: Matthew B. <[email protected]>
    Co-authored-by: Copilot Autofix powered by AI 
<[email protected]>
---
 .../app/common/type/datasetVersionFileTree.spec.ts | 241 +++++++++++++++++++++
 1 file changed, 241 insertions(+)

diff --git a/frontend/src/app/common/type/datasetVersionFileTree.spec.ts 
b/frontend/src/app/common/type/datasetVersionFileTree.spec.ts
new file mode 100644
index 0000000000..76aa8f286d
--- /dev/null
+++ b/frontend/src/app/common/type/datasetVersionFileTree.spec.ts
@@ -0,0 +1,241 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {
+  DatasetFileNode,
+  DatasetVersionFileTreeManager,
+  getFullPathFromDatasetFileNode,
+  getPathsUnderOrEqualDatasetFileNode,
+  getRelativePathFromDatasetFileNode,
+} from "./datasetVersionFileTree";
+
+describe("getFullPathFromDatasetFileNode", () => {
+  it("joins parentDir and name with a slash", () => {
+    const node: DatasetFileNode = { name: "c.txt", type: "file", parentDir: 
"/a/b" };
+    expect(getFullPathFromDatasetFileNode(node)).toBe("/a/b/c.txt");
+  });
+
+  it("produces a leading slash for a node whose parentDir is empty", () => {
+    const node: DatasetFileNode = { name: "root", type: "directory", 
parentDir: "" };
+    expect(getFullPathFromDatasetFileNode(node)).toBe("/root");
+  });
+});
+
+describe("getRelativePathFromDatasetFileNode", () => {
+  it("strips the first three path segments", () => {
+    const node: DatasetFileNode = { name: "file.csv", type: "file", parentDir: 
"/owner/dataset/v1" };
+    // full path is /owner/dataset/v1/file.csv -> segments [owner, dataset, 
v1, file.csv]
+    expect(getRelativePathFromDatasetFileNode(node)).toBe("file.csv");
+  });
+
+  it("preserves nested relative segments beyond the first three", () => {
+    const node: DatasetFileNode = { name: "f.txt", type: "file", parentDir: 
"/owner/dataset/v1/sub/dir" };
+    expect(getRelativePathFromDatasetFileNode(node)).toBe("sub/dir/f.txt");
+  });
+
+  it("returns an empty string when there are three or fewer segments", () => {
+    const node: DatasetFileNode = { name: "v1", type: "directory", parentDir: 
"/owner/dataset" };
+    // full path /owner/dataset/v1 -> exactly 3 segments -> no relative path
+    expect(getRelativePathFromDatasetFileNode(node)).toBe("");
+  });
+
+  it("ignores empty segments from duplicate slashes when counting", () => {
+    const node: DatasetFileNode = { name: "file.csv", type: "file", parentDir: 
"/owner//dataset/v1" };
+    // empty segment between the duplicate slashes is filtered out, leaving 4 
real segments
+    expect(getRelativePathFromDatasetFileNode(node)).toBe("file.csv");
+  });
+});
+
+describe("getPathsUnderOrEqualDatasetFileNode", () => {
+  it("returns the single path for a file node", () => {
+    const file: DatasetFileNode = { name: "a.txt", type: "file", parentDir: 
"/dir" };
+    expect(getPathsUnderOrEqualDatasetFileNode(file)).toEqual(["/dir/a.txt"]);
+  });
+
+  it("collects every file path under a directory", () => {
+    const file1: DatasetFileNode = { name: "file1.txt", type: "file", 
parentDir: "/dir" };
+    const file2: DatasetFileNode = { name: "file2.txt", type: "file", 
parentDir: "/dir" };
+    const dir: DatasetFileNode = { name: "dir", type: "directory", parentDir: 
"", children: [file1, file2] };
+    
expect(getPathsUnderOrEqualDatasetFileNode(dir)).toEqual(["/dir/file1.txt", 
"/dir/file2.txt"]);
+  });
+
+  it("recurses into nested directories", () => {
+    const deepFile: DatasetFileNode = { name: "deep.txt", type: "file", 
parentDir: "/a/b" };
+    const subDir: DatasetFileNode = { name: "b", type: "directory", parentDir: 
"/a", children: [deepFile] };
+    const topDir: DatasetFileNode = { name: "a", type: "directory", parentDir: 
"", children: [subDir] };
+    
expect(getPathsUnderOrEqualDatasetFileNode(topDir)).toEqual(["/a/b/deep.txt"]);
+  });
+
+  it("returns an empty array for an empty directory", () => {
+    const emptyChildren: DatasetFileNode = { name: "dir", type: "directory", 
parentDir: "", children: [] };
+    const noChildrenProp: DatasetFileNode = { name: "dir", type: "directory", 
parentDir: "" };
+    expect(getPathsUnderOrEqualDatasetFileNode(emptyChildren)).toEqual([]);
+    expect(getPathsUnderOrEqualDatasetFileNode(noChildrenProp)).toEqual([]);
+  });
+});
+
+describe("DatasetVersionFileTreeManager", () => {
+  describe("addNodeWithPath", () => {
+    it("starts with no root nodes", () => {
+      const manager = new DatasetVersionFileTreeManager();
+      expect(manager.getRootNodes()).toEqual([]);
+    });
+
+    it("builds the intermediate directory structure and returns the leaf file 
node", () => {
+      const manager = new DatasetVersionFileTreeManager();
+      const leaf = manager.addNodeWithPath("/a/b/c.txt");
+
+      expect(leaf.name).toBe("c.txt");
+      expect(leaf.type).toBe("file");
+      expect(getFullPathFromDatasetFileNode(leaf)).toBe("/a/b/c.txt");
+
+      const roots = manager.getRootNodes();
+      expect(roots.length).toBe(1);
+      expect(roots[0].name).toBe("a");
+      expect(roots[0].type).toBe("directory");
+
+      const dirB = roots[0].children![0];
+      expect(dirB.name).toBe("b");
+      expect(dirB.type).toBe("directory");
+      expect(dirB.children![0]).toBe(leaf);
+    });
+
+    it("is idempotent when adding the same path twice", () => {
+      const manager = new DatasetVersionFileTreeManager();
+      const first = manager.addNodeWithPath("/a/b/c.txt");
+      const second = manager.addNodeWithPath("/a/b/c.txt");
+
+      expect(second).toBe(first);
+      expect(manager.getRootNodes().length).toBe(1);
+      const dirB = manager.getRootNodes()[0].children![0];
+      expect(dirB.children!.length).toBe(1);
+    });
+
+    it("adds siblings under an existing directory", () => {
+      const manager = new DatasetVersionFileTreeManager();
+      manager.addNodeWithPath("/a/b/c.txt");
+      manager.addNodeWithPath("/a/b/d.txt");
+
+      const dirB = manager.getRootNodes()[0].children![0];
+      expect(dirB.children!.map(child => child.name).sort()).toEqual(["c.txt", 
"d.txt"]);
+    });
+
+    it("handles paths without a leading slash", () => {
+      const manager = new DatasetVersionFileTreeManager();
+      const leaf = manager.addNodeWithPath("x/y.txt");
+      expect(getFullPathFromDatasetFileNode(leaf)).toBe("/x/y.txt");
+      expect(manager.getRootNodes()[0].name).toBe("x");
+    });
+  });
+
+  describe("initializeWithRootNodes / constructor", () => {
+    it("exposes provided root nodes", () => {
+      const dir: DatasetFileNode = {
+        name: "dir",
+        type: "directory",
+        parentDir: "/",
+        children: [{ name: "f.txt", type: "file", parentDir: "/dir" }],
+      };
+      const manager = new DatasetVersionFileTreeManager([dir]);
+      expect(manager.getRootNodes()).toEqual([dir]);
+    });
+  });
+
+  describe("removeNode", () => {
+    it("removes a leaf node found by identity via BFS", () => {
+      const manager = new DatasetVersionFileTreeManager();
+      const leaf = manager.addNodeWithPath("/a/b/c.txt");
+      const dirB = manager.getRootNodes()[0].children![0];
+
+      manager.removeNode(leaf);
+      expect(dirB.children).toEqual([]);
+    });
+
+    it("removes a whole subtree when removing an inner directory", () => {
+      const manager = new DatasetVersionFileTreeManager();
+      manager.addNodeWithPath("/a/b/c.txt");
+      const rootA = manager.getRootNodes()[0];
+
+      manager.removeNode(rootA);
+      expect(manager.getRootNodes()).toEqual([]);
+    });
+
+    it("does nothing for a node that is not present in the tree", () => {
+      const manager = new DatasetVersionFileTreeManager();
+      manager.addNodeWithPath("/a/b/c.txt");
+      const stranger: DatasetFileNode = { name: "z.txt", type: "file", 
parentDir: "/q" };
+
+      manager.removeNode(stranger);
+      expect(manager.getRootNodes().length).toBe(1);
+      expect(manager.getRootNodes()[0].children![0].children!.length).toBe(1);
+    });
+
+    it("refuses to remove the synthetic root node", () => {
+      const manager = new DatasetVersionFileTreeManager();
+      manager.addNodeWithPath("/a/b/c.txt");
+      const fakeRoot: DatasetFileNode = { name: "/", type: "directory", 
parentDir: "" };
+
+      manager.removeNode(fakeRoot);
+      expect(manager.getRootNodes().length).toBe(1);
+    });
+  });
+
+  describe("removeNodeWithPath", () => {
+    it("removes a node from its parent's children and the internal map", () => 
{
+      const file1: DatasetFileNode = { name: "file1.txt", type: "file", 
parentDir: "/dir" };
+      const file2: DatasetFileNode = { name: "file2.txt", type: "file", 
parentDir: "/dir" };
+      const dir: DatasetFileNode = { name: "dir", type: "directory", 
parentDir: "/", children: [file1, file2] };
+      const manager = new DatasetVersionFileTreeManager([dir]);
+
+      manager.removeNodeWithPath("/dir/file1.txt");
+      expect(dir.children!.map(child => child.name)).toEqual(["file2.txt"]);
+
+      // A second removal of the same (now absent) path is a no-op.
+      manager.removeNodeWithPath("/dir/file1.txt");
+      expect(dir.children!.map(child => child.name)).toEqual(["file2.txt"]);
+    });
+
+    it("removes a whole subtree when removing a directory path", () => {
+      const file: DatasetFileNode = { name: "f.txt", type: "file", parentDir: 
"/dir/sub" };
+      const subDir: DatasetFileNode = { name: "sub", type: "directory", 
parentDir: "/dir", children: [file] };
+      const dir: DatasetFileNode = { name: "dir", type: "directory", 
parentDir: "/", children: [subDir] };
+      const manager = new DatasetVersionFileTreeManager([dir]);
+
+      manager.removeNodeWithPath("/dir/sub");
+      expect(dir.children).toEqual([]);
+
+      // Removing a descendant path after the subtree is gone is also a no-op.
+      manager.removeNodeWithPath("/dir/sub/f.txt");
+      expect(dir.children).toEqual([]);
+    });
+
+    it("does nothing for an unknown path", () => {
+      const dir: DatasetFileNode = {
+        name: "dir",
+        type: "directory",
+        parentDir: "/",
+        children: [{ name: "f.txt", type: "file", parentDir: "/dir" }],
+      };
+      const manager = new DatasetVersionFileTreeManager([dir]);
+
+      manager.removeNodeWithPath("/does/not/exist");
+      expect(dir.children!.length).toBe(1);
+    });
+  });
+});

Reply via email to