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

xiazcy pushed a commit to branch multi-label-experiment
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git

commit 2f26659d8d84bca573ef4758d6500618fcde40f3
Author: Yang Xia <[email protected]>
AuthorDate: Wed Jun 24 08:34:11 2026 -0700

    Make TinkerGraph default to ONE for labels, add strategy to throw for 
unintended drop() usage on labels()
---
 .../gremlin-server/gremlin-server-integration.yaml |   3 +
 .../process/traversal/TraversalStrategies.java     |   3 +
 .../step/map/AbstractAddVertexStepPlaceholder.java |   6 +
 .../traversal/step/map/AddVertexStartStep.java     |  15 +-
 .../step/map/AddVertexStartStepPlaceholder.java    |  13 +-
 .../LabelsDropVerificationStrategy.java            |  66 ++++
 .../apache/tinkerpop/gremlin/structure/Graph.java  |  23 +-
 .../gremlin/structure/LabelCardinality.java        | 113 +++---
 .../structure/util/detached/DetachedVertex.java    |   2 +-
 .../LabelsDropVerificationStrategyTest.java        |  85 +++++
 .../Gherkin/CommonSteps.cs                         |  30 +-
 .../Gherkin/GherkinTestRunner.cs                   |   6 -
 .../Gherkin/IgnoreException.cs                     |   7 +-
 .../Gherkin/ScenarioData.cs                        |  18 +
 gremlin-go/driver/cucumber/cucumberSteps_test.go   |  25 +-
 gremlin-go/driver/cucumber/cucumberWorld.go        |  22 +-
 .../test/cucumber/feature-steps.js                 |  17 +-
 .../gremlin-javascript/test/cucumber/world.js      |  27 +-
 .../src/main/python/tests/feature/feature_steps.py |  21 +-
 .../src/main/python/tests/feature/terrain.py       |  10 +
 .../conf/tinkergraph-multilabel.properties         |  21 ++
 .../gremlin/driver/remote/RemoteWorld.java         |  18 +
 .../server/util/CheckedGraphManagerTest.java       |   2 +-
 .../server/util/DefaultGraphManagerTest.java       |  10 +-
 .../gremlin/server/gremlin-server-integration.yaml |   3 +
 .../test/scripts/tinkergraph-multilabel.properties |  21 ++
 .../tinkerpop/gremlin/features/StepDefinition.java |  14 +-
 .../apache/tinkerpop/gremlin/features/World.java   |  12 +
 .../gremlin/test/features/map/Labels.feature       |   4 +-
 .../test/features/sideEffect/AddLabel.feature      |   2 +-
 .../test/features/sideEffect/DropLabel.feature     |   4 +-
 .../tinkergraph/structure/AbstractTinkerGraph.java |  11 -
 .../gremlin/tinkergraph/structure/TinkerEdge.java  |  45 +--
 .../gremlin/tinkergraph/structure/TinkerGraph.java |   8 +-
 .../structure/TinkerTransactionGraph.java          |   8 +-
 .../tinkergraph/structure/TinkerVertex.java        |  60 ++--
 .../tinkerpop/gremlin/tinkergraph/TinkerWorld.java |   1 +
 .../process/traversal/step/map/LabelsStepTest.java |  10 +-
 .../traversal/step/map/MergeVMultiLabelTest.java   |   6 +-
 .../step/sideEffect/LabelMutationPropertyTest.java |   6 +-
 .../step/sideEffect/LabelMutationStepTest.java     |  15 +-
 .../structure/LabelCardinalityTest.java            | 396 +++++++++++++++++++++
 .../LabelReplacePatternValidationTest.java         |   7 +-
 .../structure/MergeOnMatchLabelPatternsTest.java   |   7 +-
 .../TinkerVertexMultiLabelGremlinLangTest.java     |   7 +-
 .../structure/TinkerVertexMultiLabelTest.java      |  24 +-
 46 files changed, 976 insertions(+), 258 deletions(-)

diff --git a/docker/gremlin-server/gremlin-server-integration.yaml 
b/docker/gremlin-server/gremlin-server-integration.yaml
index 1880b34810..a730970f85 100644
--- a/docker/gremlin-server/gremlin-server-integration.yaml
+++ b/docker/gremlin-server/gremlin-server-integration.yaml
@@ -23,6 +23,9 @@ graphs: {
   graph: {
     configuration: conf/tinkergraph-service.properties,
     traversalSources: [{name: g}, {name: ggraph}]},
+  multilabel: {
+    configuration: conf/tinkergraph-multilabel.properties,
+    traversalSources: [{name: gmultilabel}]},
   immutable: {
     configuration: conf/tinkergraph-service.properties,
     traversalSources: [{name: gimmutable}]},
diff --git 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/TraversalStrategies.java
 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/TraversalStrategies.java
index bccb983665..509b9b7d8d 100644
--- 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/TraversalStrategies.java
+++ 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/TraversalStrategies.java
@@ -54,6 +54,7 @@ import 
org.apache.tinkerpop.gremlin.process.traversal.strategy.optimization.Prod
 import 
org.apache.tinkerpop.gremlin.process.traversal.strategy.optimization.RepeatUnrollStrategy;
 import 
org.apache.tinkerpop.gremlin.process.traversal.strategy.verification.ComputerVerificationStrategy;
 import 
org.apache.tinkerpop.gremlin.process.traversal.strategy.verification.EdgeLabelVerificationStrategy;
+import 
org.apache.tinkerpop.gremlin.process.traversal.strategy.verification.LabelsDropVerificationStrategy;
 import 
org.apache.tinkerpop.gremlin.process.traversal.strategy.verification.LambdaRestrictionStrategy;
 import 
org.apache.tinkerpop.gremlin.process.traversal.strategy.verification.ReadOnlyStrategy;
 import 
org.apache.tinkerpop.gremlin.process.traversal.strategy.verification.ReservedKeysVerificationStrategy;
@@ -263,6 +264,7 @@ public interface TraversalStrategies extends Serializable, 
Cloneable, Iterable<T
 
             // verification
             put(EdgeLabelVerificationStrategy.class.getSimpleName(), 
EdgeLabelVerificationStrategy.class);
+            put(LabelsDropVerificationStrategy.class.getSimpleName(), 
LabelsDropVerificationStrategy.class);
             put(LambdaRestrictionStrategy.class.getSimpleName(), 
LambdaRestrictionStrategy.class);
             put(ReadOnlyStrategy.class.getSimpleName(), 
ReadOnlyStrategy.class);
             put(ReservedKeysVerificationStrategy.class.getSimpleName(), 
ReservedKeysVerificationStrategy.class);
@@ -287,6 +289,7 @@ public interface TraversalStrategies extends Serializable, 
Cloneable, Iterable<T
                     LazyBarrierStrategy.instance(),
                     ProfileStrategy.instance(),
                     StandardVerificationStrategy.instance(),
+                    LabelsDropVerificationStrategy.instance(),
                     GValueReductionStrategy.instance());
             registerStrategies(Graph.class, graphStrategies);
             registerStrategies(EmptyGraph.class, new 
DefaultTraversalStrategies());
diff --git 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AbstractAddVertexStepPlaceholder.java
 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AbstractAddVertexStepPlaceholder.java
index 3e0f0c1c0f..f14e9998d0 100644
--- 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AbstractAddVertexStepPlaceholder.java
+++ 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AbstractAddVertexStepPlaceholder.java
@@ -57,6 +57,12 @@ public abstract class AbstractAddVertexStepPlaceholder<S> 
extends AbstractAddEle
         return Vertex.DEFAULT_LABEL;
     }
 
+    @Override
+    public void setLabel(Object label) {
+        super.setLabel(label);
+        userProvidedLabel = true;
+    }
+
     @Override
     public boolean hasUserProvidedLabel() {
         return userProvidedLabel;
diff --git 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddVertexStartStep.java
 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddVertexStartStep.java
index 5f109ae426..4e256fc96b 100644
--- 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddVertexStartStep.java
+++ 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddVertexStartStep.java
@@ -53,24 +53,27 @@ public class AddVertexStartStep extends 
AbstractStep<Vertex, Vertex> implements
 
     public AddVertexStartStep(final Traversal.Admin traversal, final String 
label) {
         super(traversal);
-        this.internalParameters.set(this, T.label, null == label ? 
Vertex.DEFAULT_LABEL : label);
+        if (label != null) {
+            this.internalParameters.set(this, T.label, label);
+        }
         userProvidedLabel = label != null;
     }
 
     public AddVertexStartStep(final Traversal.Admin traversal, final 
Traversal<?, String> vertexLabelTraversal) {
         super(traversal);
-        this.internalParameters.set(this, T.label, null == 
vertexLabelTraversal ? Vertex.DEFAULT_LABEL : vertexLabelTraversal);
+        if (vertexLabelTraversal != null) {
+            this.internalParameters.set(this, T.label, vertexLabelTraversal);
+        }
         userProvidedLabel = vertexLabelTraversal != null;
     }
 
     public AddVertexStartStep(final Traversal.Admin traversal, final 
Set<String> labels) {
         super(traversal);
-        if (labels == null || labels.isEmpty()) {
-            this.internalParameters.set(this, T.label, Vertex.DEFAULT_LABEL);
-            userProvidedLabel = false;
-        } else {
+        if (labels != null && !labels.isEmpty()) {
             this.internalParameters.set(this, T.label, labels);
             userProvidedLabel = true;
+        } else {
+            userProvidedLabel = false;
         }
     }
 
diff --git 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddVertexStartStepPlaceholder.java
 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddVertexStartStepPlaceholder.java
index a1b5fa2449..24c25d3ad4 100644
--- 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddVertexStartStepPlaceholder.java
+++ 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddVertexStartStepPlaceholder.java
@@ -19,7 +19,6 @@
 package org.apache.tinkerpop.gremlin.process.traversal.step.map;
 
 import org.apache.tinkerpop.gremlin.process.traversal.Traversal;
-import org.apache.tinkerpop.gremlin.process.traversal.lambda.ConstantTraversal;
 import org.apache.tinkerpop.gremlin.process.traversal.step.GValue;
 import org.apache.tinkerpop.gremlin.process.traversal.step.GValueHolder;
 import org.apache.tinkerpop.gremlin.structure.Vertex;
@@ -30,16 +29,15 @@ public class AddVertexStartStepPlaceholder extends 
AbstractAddVertexStepPlacehol
         implements AddVertexStepContract<Vertex>, GValueHolder<Vertex, Vertex> 
{
 
     public AddVertexStartStepPlaceholder(final Traversal.Admin traversal, 
final String label) {
-        super(traversal, label == null ? Vertex.DEFAULT_LABEL : label);
+        super(traversal, label);
     }
 
     public AddVertexStartStepPlaceholder(final Traversal.Admin traversal, 
final GValue<String> label) {
-        super(traversal, label == null ? GValue.of(Vertex.DEFAULT_LABEL) : 
label);
+        super(traversal, label);
     }
 
     public AddVertexStartStepPlaceholder(final Traversal.Admin traversal, 
final Traversal.Admin<?,String> vertexLabelTraversal) {
-        super(traversal, vertexLabelTraversal == null ?
-                new ConstantTraversal<>(Vertex.DEFAULT_LABEL) : 
(Traversal.Admin<Vertex,String>) vertexLabelTraversal);
+        super(traversal, vertexLabelTraversal == null ? null : 
(Traversal.Admin<Vertex,String>) vertexLabelTraversal);
     }
 
     public AddVertexStartStepPlaceholder(final Traversal.Admin traversal, 
final Set<String> labels) {
@@ -56,7 +54,10 @@ public class AddVertexStartStepPlaceholder extends 
AbstractAddVertexStepPlacehol
         } else if (label instanceof GValue) {
             step = new AddVertexStartStep(traversal, ((GValue<String>) 
label).get());
         } else {
-            step = new AddVertexStartStep(traversal, (String) label);
+            // When userProvidedLabel is false, label may be the default from 
the placeholder
+            // hierarchy. Pass null so AddVertexStartStep does not inject 
T.label.
+            final String labelStr = hasUserProvidedLabel() ? (String) label : 
null;
+            step = new AddVertexStartStep(traversal, labelStr);
         }
         super.configureConcreteStep(step);
         return step;
diff --git 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/strategy/verification/LabelsDropVerificationStrategy.java
 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/strategy/verification/LabelsDropVerificationStrategy.java
new file mode 100644
index 0000000000..b13a5b1526
--- /dev/null
+++ 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/strategy/verification/LabelsDropVerificationStrategy.java
@@ -0,0 +1,66 @@
+/*
+ * 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.
+ */
+package org.apache.tinkerpop.gremlin.process.traversal.strategy.verification;
+
+import org.apache.tinkerpop.gremlin.process.traversal.Step;
+import org.apache.tinkerpop.gremlin.process.traversal.Traversal;
+import org.apache.tinkerpop.gremlin.process.traversal.TraversalStrategy;
+import org.apache.tinkerpop.gremlin.process.traversal.step.filter.DropStep;
+import org.apache.tinkerpop.gremlin.process.traversal.step.filter.FilterStep;
+import org.apache.tinkerpop.gremlin.process.traversal.step.map.LabelsStep;
+import 
org.apache.tinkerpop.gremlin.process.traversal.strategy.AbstractTraversalStrategy;
+import org.apache.tinkerpop.gremlin.process.traversal.util.TraversalHelper;
+
+/**
+ * Prevents {@code labels().drop()} patterns in traversals. Users should use
+ * {@code dropLabel(label)} or {@code dropLabels()} instead.
+ *
+ * @since 4.0.0
+ */
+public final class LabelsDropVerificationStrategy
+        extends 
AbstractTraversalStrategy<TraversalStrategy.VerificationStrategy>
+        implements TraversalStrategy.VerificationStrategy {
+
+    private static final LabelsDropVerificationStrategy INSTANCE = new 
LabelsDropVerificationStrategy();
+
+    private LabelsDropVerificationStrategy() {
+    }
+
+    @Override
+    public void apply(final Traversal.Admin<?, ?> traversal) {
+        for (final DropStep<?> dropStep : 
TraversalHelper.getStepsOfClass(DropStep.class, traversal)) {
+            Step<?, ?> current = dropStep.getPreviousStep();
+
+            // Walk backward through filter steps (IsStep, HasStep, WhereStep, 
NotStep, etc.)
+            while (current instanceof FilterStep) {
+                current = current.getPreviousStep();
+            }
+
+            if (current instanceof LabelsStep) {
+                throw new VerificationException(
+                        "labels().drop() is not supported. Use 
dropLabel(label) or dropLabels() instead.",
+                        traversal);
+            }
+        }
+    }
+
+    public static LabelsDropVerificationStrategy instance() {
+        return INSTANCE;
+    }
+}
diff --git 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/Graph.java 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/Graph.java
index fe6291e616..e8ec7e9fd7 100644
--- 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/Graph.java
+++ 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/Graph.java
@@ -660,11 +660,11 @@ public interface Graph extends AutoCloseable, Host {
              * Gets the {@link LabelCardinality} for vertices in this graph. 
Defines how many labels
              * a vertex can have and whether labels are mutable.
              *
-             * @return the label cardinality for vertices, defaulting to 
{@link LabelCardinality#ZERO_OR_MORE}
+             * @return the label cardinality for vertices, defaulting to 
{@link LabelCardinality#ONE}
              * @since 4.0.0
              */
             default LabelCardinality getLabelCardinality() {
-                return LabelCardinality.ZERO_OR_MORE;
+                return LabelCardinality.ONE;
             }
 
             /**
@@ -727,25 +727,14 @@ public interface Graph extends AutoCloseable, Host {
             }
 
             /**
-             * Gets the {@link LabelCardinality} for edges in this graph. 
Defines how many labels
-             * an edge can have and whether labels are mutable.
+             * Gets the {@link LabelCardinality} for edges in this graph. Edge 
labels are always
+             * immutable (exactly one label, set at creation time).
              *
-             * @return the label cardinality for edges, defaulting to {@link 
LabelCardinality#ZERO_OR_ONE}
+             * @return the label cardinality for edges, always {@link 
LabelCardinality#ONE}
              * @since 4.0.0
              */
             default LabelCardinality getLabelCardinality() {
-                return LabelCardinality.ZERO_OR_ONE;
-            }
-
-            /**
-             * Gets the default label returned for edges with no explicit 
labels when the cardinality
-             * requires at least one label ({@link LabelCardinality#ONE} or 
{@link LabelCardinality#ONE_OR_MORE}).
-             *
-             * @return the default edge label, typically {@link 
Edge#DEFAULT_LABEL}
-             * @since 4.0.0
-             */
-            default String getDefaultLabel() {
-                return Edge.DEFAULT_LABEL;
+                return LabelCardinality.ONE;
             }
         }
 
diff --git 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/LabelCardinality.java
 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/LabelCardinality.java
index 690fa75cef..28605e60d9 100644
--- 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/LabelCardinality.java
+++ 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/LabelCardinality.java
@@ -22,17 +22,8 @@ import java.util.HashSet;
 import java.util.Set;
 
 /**
- * Defines the label cardinality for graph elements (vertices and edges). 
Providers declare their
- * supported cardinality via {@link Graph.Features} and use the validation 
methods to enforce
- * label mutation constraints.
- * <p>
- * The four cardinality modes are:
- * <ul>
- *   <li>{@link #ONE} — Exactly one label, immutable (classic TinkerPop 3.x 
behavior)</li>
- *   <li>{@link #ZERO_OR_ONE} — Zero or one label, mutable, droppable to 
zero</li>
- *   <li>{@link #ONE_OR_MORE} — One or more labels, must always have at least 
one (virtual default fills when empty)</li>
- *   <li>{@link #ZERO_OR_MORE} — Zero or more labels, fully flexible</li>
- * </ul>
+ * Defines the label cardinality for graph elements. Providers declare their 
supported cardinality
+ * via {@link Graph.Features} and use the validation methods to enforce label 
mutation constraints.
  *
  * @since 4.0.0
  */
@@ -40,20 +31,14 @@ public enum LabelCardinality {
 
     /**
      * Exactly one label, immutable. All mutation operations throw.
-     * This provides backward compatibility with TinkerPop 3.x single-label 
semantics.
+     * This is the default for TinkerGraph and provides backward compatibility 
with TinkerPop 3.x.
      */
     ONE,
 
     /**
-     * Zero or one label, mutable. The element can have at most one label at a 
time.
-     * To change the label, the existing one must be dropped first.
-     */
-    ZERO_OR_ONE,
-
-    /**
-     * One or more labels, mutable. The element must always have at least one 
label.
-     * When all explicit labels are removed, the provider's default label 
(e.g., "vertex" or "edge")
-     * is returned at read time as a virtual floor.
+     * One or more labels. The element must always have at least one label.
+     * The provider's default label (e.g., "vertex") is always physically 
present.
+     * {@code dropLabels()} always throws. {@code dropLabel(x)} succeeds only 
if at least one label remains.
      */
     ONE_OR_MORE,
 
@@ -65,8 +50,6 @@ public enum LabelCardinality {
 
     /**
      * Whether this cardinality allows multiple labels on an element 
simultaneously.
-     *
-     * @return {@code true} for {@link #ONE_OR_MORE} and {@link #ZERO_OR_MORE}
      */
     public boolean supportsMultiLabel() {
         return this == ONE_OR_MORE || this == ZERO_OR_MORE;
@@ -74,80 +57,78 @@ public enum LabelCardinality {
 
     /**
      * Whether this cardinality allows an element to have zero labels.
-     *
-     * @return {@code true} for {@link #ZERO_OR_ONE} and {@link #ZERO_OR_MORE}
      */
     public boolean supportsZeroLabels() {
-        return this == ZERO_OR_ONE || this == ZERO_OR_MORE;
+        return this == ZERO_OR_MORE;
     }
 
     /**
      * Whether this cardinality allows label mutation (addLabel/dropLabel).
-     *
-     * @return {@code true} for all modes except {@link #ONE}
      */
     public boolean supportsMutation() {
         return this != ONE;
     }
 
     /**
-     * Validates that adding the specified labels would not violate this 
cardinality's constraints.
-     * Must be called BEFORE any mutation to ensure no invalid intermediate 
states.
-     *
-     * @param currentLabels the element's current label set
-     * @param label         the first label to add
-     * @param moreLabels    additional labels to add (varargs)
-     * @throws IllegalStateException if the addition would violate cardinality 
constraints
+     * Validates that adding labels would not violate cardinality constraints.
+     * For ONE: always throws (immutable).
+     * For ONE_OR_MORE and ZERO_OR_MORE: always succeeds (multi-label add is 
unrestricted).
      */
     public void validateAdd(final Set<String> currentLabels, final String 
label, final String... moreLabels) {
         if (!supportsMutation())
             throw new IllegalStateException("Label mutation is not supported 
with cardinality " + this +
                     ". Labels are immutable once assigned.");
-
-        if (!supportsMultiLabel()) {
-            // ZERO_OR_ONE: resulting set must have at most 1 element
-            // Fast path: single label + empty set → always valid
-            if (moreLabels.length == 0 && currentLabels.isEmpty())
-                return;
-
-            // Compute the resulting set size
-            final Set<String> resultSet = new HashSet<>(currentLabels);
-            resultSet.add(label);
-            for (final String l : moreLabels) {
-                resultSet.add(l);
-            }
-            if (resultSet.size() > 1)
-                throw new IllegalStateException("Cannot add label: would 
result in " + resultSet.size() +
-                        " labels but cardinality is " + this + ". Drop the 
existing label first.");
-        }
     }
 
     /**
-     * Validates that dropping a specific label would not violate this 
cardinality's constraints.
-     * For mutable cardinalities, dropping always succeeds (floor is applied 
at read time).
-     *
-     * @param currentLabels the element's current label set
-     * @param label         the label to drop
-     * @throws IllegalStateException if mutation is not supported (ONE mode)
+     * Validates that dropping specific labels would not violate cardinality 
constraints.
+     * For ONE: always throws (immutable).
+     * For ONE_OR_MORE: throws if the resulting set would be empty.
+     * For ZERO_OR_MORE: always succeeds.
      */
-    public void validateDrop(final Set<String> currentLabels, final String 
label) {
+    public void validateDrop(final Set<String> currentLabels, final String 
label, final String... moreLabels) {
         if (!supportsMutation())
             throw new IllegalStateException("Label mutation is not supported 
with cardinality " + this +
                     ". Labels are immutable once assigned.");
-        // No floor validation needed — floor is applied at read time by the 
element's labels() method
+        if (this == ONE_OR_MORE) {
+            final Set<String> result = new HashSet<>(currentLabels);
+            result.remove(label);
+            for (final String l : moreLabels) {
+                result.remove(l);
+            }
+            if (result.isEmpty())
+                throw new IllegalStateException("Cannot drop label(s): would 
leave 0 labels but cardinality " +
+                        this + " requires at least 1.");
+        }
     }
 
     /**
-     * Validates that dropping all labels would not violate this cardinality's 
constraints.
-     * For mutable cardinalities, dropping all always succeeds (floor is 
applied at read time).
-     *
-     * @param currentLabels the element's current label set
-     * @throws IllegalStateException if mutation is not supported (ONE mode)
+     * Validates that dropping all labels would not violate cardinality 
constraints.
+     * For ONE: always throws (immutable).
+     * For ONE_OR_MORE: always throws (would violate the ≥1 requirement).
+     * For ZERO_OR_MORE: always succeeds.
      */
     public void validateDropAll(final Set<String> currentLabels) {
         if (!supportsMutation())
             throw new IllegalStateException("Label mutation is not supported 
with cardinality " + this +
                     ". Labels are immutable once assigned.");
-        // Always succeeds for mutable cardinalities — floor applied at read 
time
+        if (this == ONE_OR_MORE)
+            throw new IllegalStateException("Cannot drop all labels: 
cardinality " + this +
+                    " requires at least 1 label.");
+    }
+
+    /**
+     * Validates that a set of labels is valid for vertex creation under this 
cardinality.
+     * Called during element construction to ensure the initial label set 
conforms.
+     * For ONE: requires exactly 1 label.
+     * For ONE_OR_MORE: requires at least 1 label.
+     * For ZERO_OR_MORE: any size is valid.
+     */
+    public void validateCreation(final Set<String> labels) {
+        if (this == ONE && labels.size() != 1)
+            throw new IllegalStateException("Vertex creation requires exactly 
1 label with cardinality " +
+                    this + ", got " + labels.size());
+        if (this == ONE_OR_MORE && labels.isEmpty())
+            throw new IllegalStateException("Vertex creation requires at least 
1 label with cardinality " + this);
     }
 }
diff --git 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/util/detached/DetachedVertex.java
 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/util/detached/DetachedVertex.java
index 977eb8ebcc..b4ae187bc2 100644
--- 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/util/detached/DetachedVertex.java
+++ 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/util/detached/DetachedVertex.java
@@ -128,7 +128,7 @@ public class DetachedVertex extends DetachedElement<Vertex> 
implements Vertex {
         if (this.vertexLabels != null && !this.vertexLabels.isEmpty()) {
             return this.vertexLabels.iterator().next();
         }
-        return this.label != null ? this.label : Vertex.DEFAULT_LABEL;
+        return this.label != null ? this.label : "";
     }
 
     @Override
diff --git 
a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/strategy/verification/LabelsDropVerificationStrategyTest.java
 
b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/strategy/verification/LabelsDropVerificationStrategyTest.java
new file mode 100644
index 0000000000..eefe785c2b
--- /dev/null
+++ 
b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/strategy/verification/LabelsDropVerificationStrategyTest.java
@@ -0,0 +1,85 @@
+/*
+ * 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.
+ */
+package org.apache.tinkerpop.gremlin.process.traversal.strategy.verification;
+
+import org.apache.tinkerpop.gremlin.process.traversal.P;
+import org.apache.tinkerpop.gremlin.process.traversal.Traversal;
+import org.apache.tinkerpop.gremlin.process.traversal.TraversalStrategies;
+import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__;
+import 
org.apache.tinkerpop.gremlin.process.traversal.util.DefaultTraversalStrategies;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.util.Arrays;
+
+import static org.junit.Assert.fail;
+
+/**
+ * Tests for {@link LabelsDropVerificationStrategy}.
+ * Verifies that {@code labels().drop()} patterns are rejected while unrelated 
patterns are allowed.
+ */
+@RunWith(Parameterized.class)
+public class LabelsDropVerificationStrategyTest {
+
+    @Parameterized.Parameters(name = "{0}")
+    public static Iterable<Object[]> data() {
+        return Arrays.asList(new Object[][]{
+                // Should REJECT: labels() followed by drop() (possibly with 
filter steps in between)
+                {"labels().drop()", __.labels().drop(), false},
+                {"labels().is('x').drop()", __.labels().is("x").drop(), false},
+                {"labels().where(P.eq('x')).drop()", 
__.labels().where(P.eq("x")).drop(), false},
+                {"labels().not(__.is('x')).drop()", 
__.labels().not(__.is("x")).drop(), false},
+
+                // Should ALLOW: no LabelsStep before drop
+                {"V().drop()", __.V().drop(), true},
+                {"properties().drop()", __.properties().drop(), true},
+
+                // Should ALLOW: non-filter step between labels() and drop() 
breaks the walk
+                {"labels().map(__.constant('x')).drop()", 
__.labels().map(__.constant("x")).drop(), true},
+                {"labels().order().drop()", __.labels().order().drop(), true},
+        });
+    }
+
+    @Parameterized.Parameter(value = 0)
+    public String name;
+
+    @Parameterized.Parameter(value = 1)
+    public Traversal.Admin traversal;
+
+    @Parameterized.Parameter(value = 2)
+    public boolean allow;
+
+    @Test
+    public void shouldVerifyLabelsDropPattern() {
+        final TraversalStrategies strategies = new 
DefaultTraversalStrategies();
+        strategies.addStrategies(LabelsDropVerificationStrategy.instance());
+        traversal.asAdmin().setStrategies(strategies);
+        if (allow) {
+            traversal.asAdmin().applyStrategies();
+        } else {
+            try {
+                traversal.asAdmin().applyStrategies();
+                fail("The strategy should not allow labels().drop() pattern: " 
+ name);
+            } catch (VerificationException ve) {
+                // expected
+            }
+        }
+    }
+}
diff --git 
a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/CommonSteps.cs 
b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/CommonSteps.cs
index 2c42d1a8be..9729e7f419 100644
--- a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/CommonSteps.cs
+++ b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/CommonSteps.cs
@@ -105,13 +105,27 @@ namespace Gremlin.Net.IntegrationTest.Gherkin
         [Given("the (\\w+) graph")]
         public void ChooseModernGraph(string graphName)
         {
-            if (graphName == "empty")
+            var isMultiLabel = ScenarioData.CurrentScenario != null &&
+                (ScenarioData.CurrentScenario.Tags.Any(t => t.Name == 
"@MultiLabel") ||
+                 (ScenarioData.CurrentFeature != null && 
ScenarioData.CurrentFeature.Tags.Any(t => t.Name == "@MultiLabel")));
+
+            if (isMultiLabel && graphName == "empty")
+            {
+                ScenarioData.CleanMultilabelData();
+                var data = ScenarioData.GetByGraphName("multilabel");
+                _graphName = "multilabel";
+                _g = Traversal().With(data.Connection);
+            }
+            else
             {
-                ScenarioData.CleanEmptyData();
+                if (graphName == "empty")
+                {
+                    ScenarioData.CleanEmptyData();
+                }
+                var data = ScenarioData.GetByGraphName(graphName);
+                _graphName = graphName;
+                _g = Traversal().With(data.Connection);
             }
-            var data = ScenarioData.GetByGraphName(graphName);
-            _graphName = graphName;
-            _g = Traversal().With(data.Connection);
         }
 
         [Given("using the parameter (\\w+) defined as \"(.*)\"")]
@@ -159,11 +173,15 @@ namespace Gremlin.Net.IntegrationTest.Gherkin
                 Gremlin.UseTraversal(ScenarioData.CurrentScenario!.Name, _g, 
_parameters, _sideEffects);
             traversal.Iterate();
             
-            // We may have modified the so-called `empty` graph
+            // We may have modified the so-called `empty` or `multilabel` graph
             if (_graphName == "empty")
             {
                 ScenarioData.ReloadEmptyData();
             }
+            else if (_graphName == "multilabel")
+            {
+                ScenarioData.ReloadMultilabelData();
+            }
         }
 
         [Given("an unsupported test")]
diff --git 
a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/GherkinTestRunner.cs 
b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/GherkinTestRunner.cs
index 04969b5fae..6aee902762 100644
--- 
a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/GherkinTestRunner.cs
+++ 
b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/GherkinTestRunner.cs
@@ -125,12 +125,6 @@ namespace Gremlin.Net.IntegrationTest.Gherkin
                         continue;
                     }
 
-                    if (scenario.Tags.Any(t => t.Name == "@MultiLabel"))
-                    {
-                        failedSteps.Add(scenario.Steps.First(), new 
IgnoreException(IgnoreReason.MultiLabelStepsNotSupported));
-                        continue;
-                    }
-
                     StepBlock? currentStep = null;
                     StepDefinition? stepDefinition = null;
                     foreach (var step in scenario.Steps)
diff --git 
a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/IgnoreException.cs 
b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/IgnoreException.cs
index bf3918f253..7001391004 100644
--- a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/IgnoreException.cs
+++ b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/IgnoreException.cs
@@ -70,11 +70,6 @@ namespace Gremlin.Net.IntegrationTest.Gherkin
         /// <summary>
         /// write() is not supported yet for testing
         /// </summary>
-        WriteStepTestingNotSupported,
-
-        /// <summary>
-        /// Multi-label steps (labels(), addLabel(), dropLabel(), 
dropLabels()) are not yet available in the .NET GLV
-        /// </summary>
-        MultiLabelStepsNotSupported
+        WriteStepTestingNotSupported
     }
 }
\ No newline at end of file
diff --git 
a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/ScenarioData.cs 
b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/ScenarioData.cs
index 708b3db978..6644934ef8 100644
--- a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/ScenarioData.cs
+++ b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/ScenarioData.cs
@@ -74,6 +74,12 @@ namespace Gremlin.Net.IntegrationTest.Gherkin
             g.V().Drop().Iterate();
         }
 
+        public void CleanMultilabelData()
+        {
+            var g = Traversal().With(GetByGraphName("multilabel").Connection);
+            g.V().Drop().Iterate();
+        }
+
         public void ReloadEmptyData()
         {
             var graphData = _dataPerGraph["empty"];
@@ -83,6 +89,15 @@ namespace Gremlin.Net.IntegrationTest.Gherkin
             graphData.VertexProperties = GetVertexProperties(g);
         }
 
+        public void ReloadMultilabelData()
+        {
+            var graphData = _dataPerGraph["multilabel"];
+            var g = Traversal().With(graphData.Connection);
+            graphData.Vertices = GetVertices(g);
+            graphData.Edges = GetEdges(g);
+            graphData.VertexProperties = GetVertexProperties(g);
+        }
+
         private readonly IDictionary<string, ScenarioDataPerGraph> 
_dataPerGraph;
         
         public ScenarioData(IMessageSerializer messageSerializer)
@@ -92,6 +107,9 @@ namespace Gremlin.Net.IntegrationTest.Gherkin
             var empty = new ScenarioDataPerGraph("empty", 
_connectionFactory.CreateRemoteConnection("ggraph"),
                 new Dictionary<string, Vertex>(0), new Dictionary<string, 
Edge>(), new Dictionary<string, VertexProperty>());
             _dataPerGraph.Add("empty", empty);
+            var multilabel = new ScenarioDataPerGraph("multilabel", 
_connectionFactory.CreateRemoteConnection("gmultilabel"),
+                new Dictionary<string, Vertex>(0), new Dictionary<string, 
Edge>(), new Dictionary<string, VertexProperty>());
+            _dataPerGraph.Add("multilabel", multilabel);
         }
 
         private Dictionary<string, ScenarioDataPerGraph> LoadDataPerGraph()
diff --git a/gremlin-go/driver/cucumber/cucumberSteps_test.go 
b/gremlin-go/driver/cucumber/cucumberSteps_test.go
index 21b7d95fd5..c55ba149a9 100644
--- a/gremlin-go/driver/cucumber/cucumberSteps_test.go
+++ b/gremlin-go/driver/cucumber/cucumberSteps_test.go
@@ -501,10 +501,23 @@ func (tg *tinkerPopGraph) nothingShouldHappenBecause(arg1 
*godog.DocString) erro
 
 // Choose the graph.
 func (tg *tinkerPopGraph) chooseGraph(graphName string) error {
-       tg.graphName = graphName
-       data := tg.graphDataMap[graphName]
+       // Multi-label tests use the gmultilabel traversal source for empty 
graphs
+       isMultiLabel := false
+       for _, tag := range tg.scenario.Tags {
+               if tag.Name == "@MultiLabel" {
+                       isMultiLabel = true
+                       break
+               }
+       }
+
+       if isMultiLabel && graphName == "empty" {
+               tg.graphName = "multilabel"
+       } else {
+               tg.graphName = graphName
+       }
+       data := tg.graphDataMap[tg.graphName]
        tg.g = gremlingo.Traversal_().With(data.connection).With("language", 
"gremlin-lang")
-       if graphName == "empty" {
+       if tg.graphName == "empty" || tg.graphName == "multilabel" {
                err := tg.cleanEmptyDataGraph(tg.g)
                if err != nil {
                        return err
@@ -997,6 +1010,8 @@ func (tg *tinkerPopGraph) theTraversalOf(arg1 
*godog.DocString) error {
 func (tg *tinkerPopGraph) usingTheParameterDefined(name string, params string) 
error {
        if tg.graphName == "empty" {
                tg.reloadEmptyData()
+       } else if tg.graphName == "multilabel" {
+               tg.reloadMultilabelData()
        }
        tg.parameters[name] = parseValue(strings.Replace(params, "\\\"", "\"", 
-1), tg.graphName)
        return nil
@@ -1017,6 +1032,8 @@ func (tg *tinkerPopGraph) usingTheParameterOfP(paramName, 
pVal, stringVal string
 func (tg *tinkerPopGraph) usingTheSideEffectDefined(key string, value string) 
error {
        if tg.graphName == "empty" {
                tg.reloadEmptyData()
+       } else if tg.graphName == "multilabel" {
+               tg.reloadMultilabelData()
        }
        tg.sideEffects[key] = parseValue(strings.Replace(value, "\\\"", "\"", 
-1), tg.graphName)
        return nil
@@ -1132,7 +1149,7 @@ func TestCucumberFeatures(t *testing.T) {
                TestSuiteInitializer: InitializeTestSuite,
                ScenarioInitializer:  InitializeScenario,
                Options: &godog.Options{
-                       Tags:     "~@GraphComputerOnly && 
~@AllowNullPropertyValues && ~@StepTree && ~@StepWrite && ~@DataChar && 
~@MultiLabel",
+                       Tags:     "~@GraphComputerOnly && 
~@AllowNullPropertyValues && ~@StepTree && ~@StepWrite && ~@DataChar",
                        Format:   "pretty",
                        Paths:    
[]string{getEnvOrDefaultString("CUCUMBER_FEATURE_FOLDER", 
"../../../gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features")},
                        TestingT: t, // Testing instance that will run subtests.
diff --git a/gremlin-go/driver/cucumber/cucumberWorld.go 
b/gremlin-go/driver/cucumber/cucumberWorld.go
index 206094522f..ecc50d34c5 100644
--- a/gremlin-go/driver/cucumber/cucumberWorld.go
+++ b/gremlin-go/driver/cucumber/cucumberWorld.go
@@ -87,7 +87,7 @@ func NewCucumberWorld() *CucumberWorld {
        }
 }
 
-var graphNames = []string{"modern", "classic", "crew", "grateful", "sink", 
"empty"}
+var graphNames = []string{"modern", "classic", "crew", "grateful", "sink", 
"empty", "multilabel"}
 
 func (t *CucumberWorld) getDataGraphFromMap(name string) *DataGraph {
        if val, ok := t.graphDataMap[name]; ok {
@@ -101,6 +101,8 @@ func (t *CucumberWorld) loadAllDataGraph() {
        for _, name := range graphNames {
                if name == "empty" {
                        t.loadEmptyDataGraph()
+               } else if name == "multilabel" {
+                       t.loadMultilabelDataGraph()
                } else {
                        connection, err := 
gremlingo.NewDriverRemoteConnection(scenarioUrl(),
                                func(settings 
*gremlingo.DriverRemoteConnectionSettings) {
@@ -128,6 +130,13 @@ func (t *CucumberWorld) loadEmptyDataGraph() {
        t.graphDataMap["empty"] = &DataGraph{connection: connection}
 }
 
+func (t *CucumberWorld) loadMultilabelDataGraph() {
+       connection, _ := gremlingo.NewDriverRemoteConnection(scenarioUrl(), 
func(settings *gremlingo.DriverRemoteConnectionSettings) {
+               settings.TraversalSource = "gmultilabel"
+       })
+       t.graphDataMap["multilabel"] = &DataGraph{connection: connection}
+}
+
 func (t *CucumberWorld) reloadEmptyData() {
        graphData := t.getDataGraphFromMap("empty")
        g := gremlingo.Traversal_().With(graphData.connection).With("language", 
"gremlin-lang")
@@ -135,6 +144,13 @@ func (t *CucumberWorld) reloadEmptyData() {
        graphData.edges = getEdges(g)
 }
 
+func (t *CucumberWorld) reloadMultilabelData() {
+       graphData := t.getDataGraphFromMap("multilabel")
+       g := gremlingo.Traversal_().With(graphData.connection).With("language", 
"gremlin-lang")
+       graphData.vertices = getVertices(g)
+       graphData.edges = getEdges(g)
+}
+
 func (t *CucumberWorld) cleanEmptyDataGraph(g *gremlingo.GraphTraversalSource) 
error {
        future := g.V().Drop().Iterate()
        return <-future
@@ -244,6 +260,10 @@ func (t *CucumberWorld) recreateAllDataGraphConnection() 
error {
                        t.getDataGraphFromMap(name).connection, err = 
gremlingo.NewDriverRemoteConnection(scenarioUrl(), func(settings 
*gremlingo.DriverRemoteConnectionSettings) {
                                settings.TraversalSource = "ggraph"
                        })
+               } else if name == "multilabel" {
+                       t.getDataGraphFromMap(name).connection, err = 
gremlingo.NewDriverRemoteConnection(scenarioUrl(), func(settings 
*gremlingo.DriverRemoteConnectionSettings) {
+                               settings.TraversalSource = "gmultilabel"
+                       })
                } else {
                        t.getDataGraphFromMap(name).connection, err = 
gremlingo.NewDriverRemoteConnection(scenarioUrl(), func(settings 
*gremlingo.DriverRemoteConnectionSettings) {
                                settings.TraversalSource = "g" + name
diff --git a/gremlin-js/gremlin-javascript/test/cucumber/feature-steps.js 
b/gremlin-js/gremlin-javascript/test/cucumber/feature-steps.js
index bb956a2538..66e002aaf6 100644
--- a/gremlin-js/gremlin-javascript/test/cucumber/feature-steps.js
+++ b/gremlin-js/gremlin-javascript/test/cucumber/feature-steps.js
@@ -111,7 +111,13 @@ Given(/^the (.+) graph$/, function (graphName) {
   if (ignoredScenarios[this.scenario]) {
     return 'skipped';
   }
-  this.graphName = graphName;
+
+  // Multi-label tests use the gmultilabel traversal source for empty graphs
+  if (this.isMultiLabel && graphName === 'empty') {
+    this.graphName = 'multilabel';
+  } else {
+    this.graphName = graphName;
+  }
   const data = this.getData();
   this.g = anon.traversal().with_(data.connection);
 
@@ -119,9 +125,12 @@ Given(/^the (.+) graph$/, function (graphName) {
     this.g = this.g.withComputer();
   }
 
-  if (graphName === 'empty') {
+  if (this.graphName === 'empty') {
     return this.cleanEmptyGraph();
   }
+  if (this.graphName === 'multilabel') {
+    return this.cleanMultilabelGraph();
+  }
 });
 
 Given('the graph initializer of', function (traversalText) {
@@ -151,6 +160,8 @@ Given(/^using the parameter (.+) defined as "(.+)"$/, 
function (paramName, strin
   let p = Promise.resolve();
   if (this.graphName === 'empty') {
     p = this.loadEmptyGraphData();
+  } else if (this.graphName === 'multilabel') {
+    p = this.loadMultilabelGraphData();
   }
   return p.then(() => {
     this.parameters[paramName] = parseValue.call(this, stringValue);
@@ -168,6 +179,8 @@ Given(/^using the side effect (.+) defined as "(.+)"$/, 
function (sideEffectKey,
   let p = Promise.resolve();
   if (this.graphName === 'empty') {
     p = this.loadEmptyGraphData();
+  } else if (this.graphName === 'multilabel') {
+    p = this.loadMultilabelGraphData();
   }
   return p.then(() => {
     this.sideEffects[sideEffectKey] = parseValue.call(this, stringValue);
diff --git a/gremlin-js/gremlin-javascript/test/cucumber/world.js 
b/gremlin-js/gremlin-javascript/test/cucumber/world.js
index 8dcd7d18ba..fd8e21449f 100644
--- a/gremlin-js/gremlin-javascript/test/cucumber/world.js
+++ b/gremlin-js/gremlin-javascript/test/cucumber/world.js
@@ -54,6 +54,12 @@ TinkerPopWorld.prototype.cleanEmptyGraph = function () {
   return g.V().drop().toList();
 };
 
+TinkerPopWorld.prototype.cleanMultilabelGraph = function () {
+  const connection = this.cache['multilabel'].connection;
+  const g = anon.traversal().withRemote(connection);
+  return g.V().drop().toList();
+};
+
 TinkerPopWorld.prototype.loadEmptyGraphData = function () {
   const cacheData = this.cache['empty'];
   const c = cacheData.connection;
@@ -64,16 +70,30 @@ TinkerPopWorld.prototype.loadEmptyGraphData = function () {
   });
 };
 
+TinkerPopWorld.prototype.loadMultilabelGraphData = function () {
+  const cacheData = this.cache['multilabel'];
+  const c = cacheData.connection;
+  return Promise.all([ getVertices(c), getEdges(c), getVertexProperties(c) 
]).then(values => {
+    cacheData.vertices = values[0];
+    cacheData.edges = values[1];
+    cacheData.vertexProperties = values[2]
+  });
+};
+
 setWorldConstructor(TinkerPopWorld);
 
 BeforeAll(function () {
   // load all traversals
-  const promises = ['modern', 'classic', 'crew', 'grateful', 'sink', 
'empty'].map(graphName => {
+  const promises = ['modern', 'classic', 'crew', 'grateful', 'sink', 'empty', 
'multilabel'].map(graphName => {
     let connection = null;
     if (graphName === 'empty') {
       connection = getConnection('ggraph');
       return connection.open().then(() => cache['empty'] = { connection: 
connection });
     }
+    if (graphName === 'multilabel') {
+      connection = getConnection('gmultilabel');
+      return connection.open().then(() => cache['multilabel'] = { connection: 
connection });
+    }
     connection = getConnection('g' + graphName);
     return connection.open()
       .then(() => Promise.all([getVertices(connection), getEdges(connection), 
getVertexProperties(connection)]))
@@ -96,6 +116,7 @@ AfterAll(function () {
 Before(function (info) {
   this.scenario = info.pickle.name;
   this.cache = cache;
+  this.isMultiLabel = info.pickle.tags && info.pickle.tags.some(t => t.name 
=== '@MultiLabel');
 });
 
 Before({tags: "@GraphComputerOnly"}, function() {
@@ -118,10 +139,6 @@ Before({tags: "@DataDuration"}, function() {
   return 'skipped'
 })
 
-Before({tags: "@MultiLabel"}, function() {
-  return 'skipped'
-})
-
 function getVertices(connection) {
   const g = anon.traversal().withRemote(connection);
   return g.V().group().by('name').by(__.tail()).next().then(it => {
diff --git a/gremlin-python/src/main/python/tests/feature/feature_steps.py 
b/gremlin-python/src/main/python/tests/feature/feature_steps.py
index 56feab3a29..d3529dc46d 100644
--- a/gremlin-python/src/main/python/tests/feature/feature_steps.py
+++ b/gremlin-python/src/main/python/tests/feature/feature_steps.py
@@ -90,15 +90,17 @@ def choose_graph(step, graph_name):
     if not step.context.ignore:
         step.context.ignore = "WithReservedKeysVerificationStrategy" in tagset
 
-    # Multi-label steps not yet available in Python GLV
-    if not step.context.ignore:
-        step.context.ignore = "MultiLabel" in tagset
-
     if (step.context.ignore):
         return
 
-    step.context.graph_name = graph_name
-    step.context.g = 
traversal().with_(step.context.remote_conn[graph_name]).with_('language', 
'gremlin-lang')
+    # Multi-label tests use the gmultilabel traversal source for empty graphs
+    is_multilabel = "MultiLabel" in tagset
+    if is_multilabel and graph_name == "empty":
+        step.context.graph_name = "multilabel"
+        step.context.g = 
traversal().with_(step.context.remote_conn["multilabel"]).with_('language', 
'gremlin-lang')
+    else:
+        step.context.graph_name = graph_name
+        step.context.g = 
traversal().with_(step.context.remote_conn[graph_name]).with_('language', 
'gremlin-lang')
 
 
 @given("the graph initializer of")
@@ -459,6 +461,13 @@ def __find_cached_element(ctx, graph_name, identifier, 
element_type):
             cache = world.create_lookup_vp(ctx.remote_conn["empty"])
         else:
             cache = world.create_lookup_e(ctx.remote_conn["empty"])
+    elif graph_name == "multilabel":
+        if element_type == "v":
+            cache = world.create_lookup_v(ctx.remote_conn["multilabel"])
+        elif element_type == "vp":
+            cache = world.create_lookup_vp(ctx.remote_conn["multilabel"])
+        else:
+            cache = world.create_lookup_e(ctx.remote_conn["multilabel"])
     else:
         if element_type == "v":
             cache = ctx.lookup_v[graph_name]
diff --git a/gremlin-python/src/main/python/tests/feature/terrain.py 
b/gremlin-python/src/main/python/tests/feature/terrain.py
index d81db72c19..654d1e8aa5 100644
--- a/gremlin-python/src/main/python/tests/feature/terrain.py
+++ b/gremlin-python/src/main/python/tests/feature/terrain.py
@@ -83,10 +83,20 @@ def prepare_traversal_source(scenario):
     g = traversal().with_(remote)
     g.V().drop().iterate()
 
+    # create a fresh remote for multi-label tests that need ZERO_OR_MORE 
vertex label cardinality
+    tagset = [tag.name for tag in scenario.all_tags]
+    if "MultiLabel" in tagset:
+        multilabel_remote = __create_remote("gmultilabel")
+        scenario.context.remote_conn["multilabel"] = multilabel_remote
+        gml = traversal().with_(multilabel_remote)
+        gml.V().drop().iterate()
+
 
 @after.each_scenario
 def close_traversal_source(scenario):
     scenario.context.remote_conn["empty"].close()
+    if "multilabel" in scenario.context.remote_conn:
+        scenario.context.remote_conn["multilabel"].close()
 
 
 @after.all
diff --git a/gremlin-server/conf/tinkergraph-multilabel.properties 
b/gremlin-server/conf/tinkergraph-multilabel.properties
new file mode 100644
index 0000000000..54391e8c5f
--- /dev/null
+++ b/gremlin-server/conf/tinkergraph-multilabel.properties
@@ -0,0 +1,21 @@
+# 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.
+gremlin.graph=org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerGraph
+gremlin.tinkergraph.vertexIdManager=INTEGER
+gremlin.tinkergraph.edgeIdManager=INTEGER
+gremlin.tinkergraph.vertexPropertyIdManager=LONG
+gremlin.tinkergraph.vertexLabelCardinality=ZERO_OR_MORE
diff --git 
a/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/driver/remote/RemoteWorld.java
 
b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/driver/remote/RemoteWorld.java
index 42bc9b5dee..33a0442d26 100644
--- 
a/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/driver/remote/RemoteWorld.java
+++ 
b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/driver/remote/RemoteWorld.java
@@ -23,6 +23,7 @@ import org.apache.tinkerpop.gremlin.LoadGraphWith;
 import org.apache.tinkerpop.gremlin.TestHelper;
 import org.apache.tinkerpop.gremlin.driver.Client;
 import org.apache.tinkerpop.gremlin.driver.Cluster;
+import org.apache.tinkerpop.gremlin.driver.RequestOptions;
 import org.apache.tinkerpop.gremlin.features.World;
 import org.apache.tinkerpop.gremlin.process.computer.Computer;
 import org.apache.tinkerpop.gremlin.process.traversal.AnonymousTraversalSource;
@@ -99,6 +100,18 @@ public abstract class RemoteWorld implements World {
         return 
AnonymousTraversalSource.traversal().withRemote(DriverRemoteConnection.using(client,
 remoteTraversalSource));
     }
 
+    @Override
+    public GraphTraversalSource getMultiLabelGraphTraversalSource() {
+        final Client client = cluster.connect();
+        try { // Clear data before run because tests are allowed to modify 
data for the multi-label graph.
+            final RequestOptions options = 
RequestOptions.build().addG("gmultilabel").create();
+            client.submit("g.V().drop()", options).all().get();
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+        return 
AnonymousTraversalSource.traversal().withRemote(DriverRemoteConnection.using(client,
 "gmultilabel"));
+    }
+
     @Override
     public String changePathToDataFile(final String pathToFileFromGremlin) {
         return ".." + File.separator + pathToFileFromGremlin;
@@ -137,6 +150,11 @@ public abstract class RemoteWorld implements World {
             }
         }
 
+        @Override
+        public GraphTraversalSource getMultiLabelGraphTraversalSource() {
+            throw new AssumptionViolatedException("GraphComputer does not 
support mutation");
+        }
+
         @Override
         public void beforeEachScenario(Scenario scenario) {
             super.beforeEachScenario(scenario);
diff --git 
a/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/util/CheckedGraphManagerTest.java
 
b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/util/CheckedGraphManagerTest.java
index 9fed97cf98..490ead1423 100644
--- 
a/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/util/CheckedGraphManagerTest.java
+++ 
b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/util/CheckedGraphManagerTest.java
@@ -66,7 +66,7 @@ public class CheckedGraphManagerTest {
     public void justAGraphFails() {
         settings.graphs.put("invalid", "conf/invalidPath");
         final GraphManager manager = new CheckedGraphManager(settings);
-        assertThat(manager.getGraphNames(), hasSize(7));
+        assertThat(manager.getGraphNames(), hasSize(8));
     }
 
     /**
diff --git 
a/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/util/DefaultGraphManagerTest.java
 
b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/util/DefaultGraphManagerTest.java
index 6cb6238a89..87af675779 100644
--- 
a/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/util/DefaultGraphManagerTest.java
+++ 
b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/util/DefaultGraphManagerTest.java
@@ -48,7 +48,7 @@ public class DefaultGraphManagerTest {
         final Set<String> graphNames = graphManager.getGraphNames();
 
         assertNotNull(graphNames);
-        assertEquals(7, graphNames.size());
+        assertEquals(8, graphNames.size());
 
         assertThat(graphNames.contains("graph"), is(true));
         assertThat(graphNames.contains("classic"), is(true));
@@ -68,7 +68,7 @@ public class DefaultGraphManagerTest {
         final Bindings bindings = graphManager.getAsBindings();
 
         assertNotNull(bindings);
-        assertEquals(7, bindings.size());
+        assertEquals(8, bindings.size());
         assertThat(bindings.containsKey("graph"), is(true));
         assertThat(bindings.containsKey("classic"), is(true));
         assertThat(bindings.containsKey("modern"), is(true));
@@ -99,7 +99,7 @@ public class DefaultGraphManagerTest {
 
         final Set<String> graphNames = graphManager.getGraphNames();
         assertNotNull(graphNames);
-        assertEquals(8, graphNames.size());
+        assertEquals(9, graphNames.size());
         assertThat(graphNames.contains("newGraph"), is(true));
         assertThat(graphNames.contains("graph"), is(true));
         assertThat(graphNames.contains("classic"), is(true));
@@ -120,14 +120,14 @@ public class DefaultGraphManagerTest {
         graphManager.putGraph("newGraph", graph);
         final Set<String> graphNames = graphManager.getGraphNames();
         assertNotNull(graphNames);
-        assertEquals(8, graphNames.size());
+        assertEquals(9, graphNames.size());
         assertThat(graphNames.contains("newGraph"), is(true));
         assertThat(graphManager.getGraph("newGraph"), 
instanceOf(TinkerGraph.class));
 
         graphManager.removeGraph("newGraph");
 
         final Set<String> graphNames2 = graphManager.getGraphNames();
-        assertEquals(7, graphNames2.size());
+        assertEquals(8, graphNames2.size());
         assertThat(graphNames2.contains("newGraph"), is(false));
     }
 
diff --git 
a/gremlin-server/src/test/resources/org/apache/tinkerpop/gremlin/server/gremlin-server-integration.yaml
 
b/gremlin-server/src/test/resources/org/apache/tinkerpop/gremlin/server/gremlin-server-integration.yaml
index be135f03c7..5e9a753eed 100644
--- 
a/gremlin-server/src/test/resources/org/apache/tinkerpop/gremlin/server/gremlin-server-integration.yaml
+++ 
b/gremlin-server/src/test/resources/org/apache/tinkerpop/gremlin/server/gremlin-server-integration.yaml
@@ -35,6 +35,9 @@ graphs: {
   graph: {
     configuration: conf/tinkergraph-empty.properties,
     traversalSources: [{name: g}, {name: ggraph}]},
+  multilabel: {
+    configuration: conf/tinkergraph-multilabel.properties,
+    traversalSources: [{name: gmultilabel}]},
   classic: {
     configuration: conf/tinkergraph-empty.properties,
     traversalSources: [{name: gclassic}]},
diff --git a/gremlin-server/src/test/scripts/tinkergraph-multilabel.properties 
b/gremlin-server/src/test/scripts/tinkergraph-multilabel.properties
new file mode 100644
index 0000000000..54391e8c5f
--- /dev/null
+++ b/gremlin-server/src/test/scripts/tinkergraph-multilabel.properties
@@ -0,0 +1,21 @@
+# 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.
+gremlin.graph=org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerGraph
+gremlin.tinkergraph.vertexIdManager=INTEGER
+gremlin.tinkergraph.edgeIdManager=INTEGER
+gremlin.tinkergraph.vertexPropertyIdManager=LONG
+gremlin.tinkergraph.vertexLabelCardinality=ZERO_OR_MORE
diff --git 
a/gremlin-test/src/main/java/org/apache/tinkerpop/gremlin/features/StepDefinition.java
 
b/gremlin-test/src/main/java/org/apache/tinkerpop/gremlin/features/StepDefinition.java
index 45f29c17b6..4de6072983 100644
--- 
a/gremlin-test/src/main/java/org/apache/tinkerpop/gremlin/features/StepDefinition.java
+++ 
b/gremlin-test/src/main/java/org/apache/tinkerpop/gremlin/features/StepDefinition.java
@@ -111,6 +111,7 @@ public final class StepDefinition {
     private static final ObjectMapper mapper = new ObjectMapper();
 
     private World world;
+    private Scenario currentScenario;
     private GraphTraversalSource g;
     private File tempWorkingDir;
     private final Map<String, Object> stringParameters = new HashMap<>();
@@ -316,6 +317,7 @@ public final class StepDefinition {
 
     @Before
     public void beforeEachScenario(final Scenario scenario) throws Exception {
+        this.currentScenario = scenario;
         world.beforeEachScenario(scenario);
         stringParameters.clear();
         if (traversal != null) {
@@ -355,10 +357,16 @@ public final class StepDefinition {
 
     @Given("the {word} graph")
     public void givenTheXGraph(final String graphName) {
-        if (graphName.equals("empty"))
-            this.g = world.getGraphTraversalSource(null);
-        else
+        final boolean isMultiLabel = currentScenario != null &&
+                currentScenario.getSourceTagNames().contains("@MultiLabel");
+        if (graphName.equals("empty")) {
+            if (isMultiLabel)
+                this.g = world.getMultiLabelGraphTraversalSource();
+            else
+                this.g = world.getGraphTraversalSource(null);
+        } else {
             this.g = 
world.getGraphTraversalSource(GraphData.valueOf(graphName.toUpperCase()));
+        }
     }
 
     @Given("the graph initializer of")
diff --git 
a/gremlin-test/src/main/java/org/apache/tinkerpop/gremlin/features/World.java 
b/gremlin-test/src/main/java/org/apache/tinkerpop/gremlin/features/World.java
index 0e41b877f0..c693346d78 100644
--- 
a/gremlin-test/src/main/java/org/apache/tinkerpop/gremlin/features/World.java
+++ 
b/gremlin-test/src/main/java/org/apache/tinkerpop/gremlin/features/World.java
@@ -55,6 +55,18 @@ public interface World {
      */
     public GraphTraversalSource getGraphTraversalSource(final GraphData 
graphData);
 
+    /**
+     * Gets a {@link GraphTraversalSource} configured for multi-label support 
(ZERO_OR_MORE vertex label cardinality).
+     * This source is used by {@code @MultiLabel} tagged scenarios that 
require label mutation operations such as
+     * {@code addLabel()} and {@code dropLabel()}. The default implementation 
delegates to
+     * {@link #getGraphTraversalSource(GraphData)} with {@code null} which 
works for embedded graphs that already
+     * have multi-label cardinality configured. Remote implementations should 
override this to connect to a
+     * dedicated multi-label traversal source.
+     */
+    public default GraphTraversalSource getMultiLabelGraphTraversalSource() {
+        return getGraphTraversalSource(null);
+    }
+
     /**
      * Called before each individual test is executed which provides an 
opportunity to do some setup. For example,
      * if there is a specific test that can't be supported it can be ignored 
by checking for the name with
diff --git 
a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/Labels.feature
 
b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/Labels.feature
index 322d0ca42c..a84ba88376 100644
--- 
a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/Labels.feature
+++ 
b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/Labels.feature
@@ -92,9 +92,7 @@ Feature: Step - labels()
       g.V().labels()
       """
     When iterated to list
-    Then the result should be unordered
-      | result |
-      | vertex |
+    Then the result should have a count of 0
 
   @MultiLabel
   Scenario: g_E_labels
diff --git 
a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/sideEffect/AddLabel.feature
 
b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/sideEffect/AddLabel.feature
index 0837669796..ec12441f1e 100644
--- 
a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/sideEffect/AddLabel.feature
+++ 
b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/sideEffect/AddLabel.feature
@@ -77,4 +77,4 @@ Feature: Step - addLabel()
       g.E().addLabel("friend").labels().fold()
       """
     When iterated to list
-    Then the traversal will raise an error with message containing text of 
"Cannot add label"
+    Then the traversal will raise an error with message containing text of 
"Label mutation is not supported"
diff --git 
a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/sideEffect/DropLabel.feature
 
b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/sideEffect/DropLabel.feature
index c96e375cde..9d46da0324 100644
--- 
a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/sideEffect/DropLabel.feature
+++ 
b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/sideEffect/DropLabel.feature
@@ -90,7 +90,7 @@ Feature: Step - dropLabel() / dropLabels()
       g.E().dropLabel("knows").labels().fold()
       """
     When iterated to list
-    Then the result should have a count of 1
+    Then the traversal will raise an error with message containing text of 
"Label mutation is not supported"
 
   @MultiLabel
   Scenario: g_E_dropLabels_labels
@@ -104,4 +104,4 @@ Feature: Step - dropLabel() / dropLabels()
       g.E().dropLabels().labels()
       """
     When iterated to list
-    Then the result should have a count of 0
+    Then the traversal will raise an error with message containing text of 
"Label mutation is not supported"
diff --git 
a/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/AbstractTinkerGraph.java
 
b/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/AbstractTinkerGraph.java
index 3f1f994af9..63bc2f28c8 100644
--- 
a/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/AbstractTinkerGraph.java
+++ 
b/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/AbstractTinkerGraph.java
@@ -65,7 +65,6 @@ public abstract class AbstractTinkerGraph implements Graph {
     public static final String GREMLIN_TINKERGRAPH_ALLOW_NULL_PROPERTY_VALUES 
= "gremlin.tinkergraph.allowNullPropertyValues";
     public static final String GREMLIN_TINKERGRAPH_SERVICE = 
"gremlin.tinkergraph.service";
     public static final String GREMLIN_TINKERGRAPH_VERTEX_LABEL_CARDINALITY = 
"gremlin.tinkergraph.vertexLabelCardinality";
-    public static final String GREMLIN_TINKERGRAPH_EDGE_LABEL_CARDINALITY = 
"gremlin.tinkergraph.edgeLabelCardinality";
 
     protected AtomicLong currentId = new AtomicLong(-1L);
     protected Map<Object, VertexProperty> vertexProperties = new 
ConcurrentHashMap<>();
@@ -423,16 +422,6 @@ public abstract class AbstractTinkerGraph implements Graph 
{
         public boolean willAllowId(final Object id) {
             return edgeIdManager.allow(id);
         }
-
-        @Override
-        public LabelCardinality getLabelCardinality() {
-            return edgeLabelCardinality;
-        }
-
-        @Override
-        public String getDefaultLabel() {
-            return defaultEdgeLabel;
-        }
     }
 
     public class TinkerGraphVertexPropertyFeatures implements 
Features.VertexPropertyFeatures {
diff --git 
a/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerEdge.java
 
b/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerEdge.java
index c55b3a30ce..c1798c998b 100644
--- 
a/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerEdge.java
+++ 
b/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerEdge.java
@@ -29,7 +29,6 @@ import 
org.apache.tinkerpop.gremlin.structure.util.StringFactory;
 import org.apache.tinkerpop.gremlin.util.iterator.IteratorUtils;
 
 import java.util.Collections;
-import java.util.HashSet;
 import java.util.Iterator;
 import java.util.LinkedHashSet;
 import java.util.Map;
@@ -132,21 +131,12 @@ public class TinkerEdge extends TinkerElement implements 
Edge {
 
     @Override
     public Set<String> labels() {
-        if (this.edgeLabels.isEmpty()) {
-            if (!this.graph.edgeLabelCardinality.supportsZeroLabels()) {
-                return Collections.singleton(this.graph.defaultEdgeLabel);
-            }
-            return Collections.emptySet();
-        }
         return Collections.unmodifiableSet(this.edgeLabels);
     }
 
     @Override
     @Deprecated
     public String label() {
-        if (this.edgeLabels.isEmpty()) {
-            return this.graph.edgeLabelCardinality.supportsZeroLabels() ? null 
: this.graph.defaultEdgeLabel;
-        }
         return this.edgeLabels.iterator().next();
     }
 
@@ -159,48 +149,21 @@ public class TinkerEdge extends TinkerElement implements 
Edge {
         }
 
         this.graph.edgeLabelCardinality.validateAdd(this.edgeLabels, label, 
labels);
-
-        this.edgeLabels.add(label);
-        this.graph.addEdgeToAdjacency(this, label);
-        for (final String l : labels) {
-            this.edgeLabels.add(l);
-            this.graph.addEdgeToAdjacency(this, l);
-        }
-        this.label = this.edgeLabels.iterator().next();
+        // Never reached — validateAdd throws for ONE
     }
 
     @Override
     public void dropLabels() {
         graph.touch(this);
         this.graph.edgeLabelCardinality.validateDropAll(this.edgeLabels);
-
-        for (final String l : new HashSet<>(this.edgeLabels)) {
-            this.graph.removeEdgeFromAdjacency(this, l);
-        }
-        this.edgeLabels.clear();
-        this.label = this.edgeLabels.isEmpty()
-                ? (this.graph.edgeLabelCardinality.supportsZeroLabels() ? null 
: this.graph.defaultEdgeLabel)
-                : this.edgeLabels.iterator().next();
+        // Never reached — validateDropAll throws for ONE
     }
 
     @Override
     public void dropLabel(final String label, final String... labels) {
         graph.touch(this);
-        this.graph.edgeLabelCardinality.validateDrop(this.edgeLabels, label);
-
-        if (this.edgeLabels.contains(label)) {
-            this.graph.removeEdgeFromAdjacency(this, label);
-            this.edgeLabels.remove(label);
-        }
-        for (final String l : labels) {
-            if (this.edgeLabels.contains(l)) {
-                this.graph.removeEdgeFromAdjacency(this, l);
-                this.edgeLabels.remove(l);
-            }
-        }
-        this.label = this.edgeLabels.isEmpty()
-                ? (this.graph.edgeLabelCardinality.supportsZeroLabels() ? null 
: this.graph.defaultEdgeLabel)
-                : this.edgeLabels.iterator().next();
+        this.graph.edgeLabelCardinality.validateDrop(this.edgeLabels, label, 
labels);
+        // Never reached — validateDrop throws for ONE
     }
 
     @Override
diff --git 
a/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerGraph.java
 
b/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerGraph.java
index b47810dc75..b90cf87709 100644
--- 
a/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerGraph.java
+++ 
b/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerGraph.java
@@ -88,9 +88,8 @@ public class TinkerGraph extends AbstractTinkerGraph {
         allowNullPropertyValues = 
configuration.getBoolean(GREMLIN_TINKERGRAPH_ALLOW_NULL_PROPERTY_VALUES, false);
 
         vertexLabelCardinality = LabelCardinality.valueOf(
-                
configuration.getString(GREMLIN_TINKERGRAPH_VERTEX_LABEL_CARDINALITY, 
LabelCardinality.ZERO_OR_MORE.name()));
-        edgeLabelCardinality = LabelCardinality.valueOf(
-                
configuration.getString(GREMLIN_TINKERGRAPH_EDGE_LABEL_CARDINALITY, 
LabelCardinality.ZERO_OR_ONE.name()));
+                
configuration.getString(GREMLIN_TINKERGRAPH_VERTEX_LABEL_CARDINALITY, 
LabelCardinality.ONE.name()));
+        edgeLabelCardinality = LabelCardinality.ONE;
         defaultVertexLabel = Vertex.DEFAULT_LABEL;
         defaultEdgeLabel = Edge.DEFAULT_LABEL;
 
@@ -143,8 +142,7 @@ public class TinkerGraph extends AbstractTinkerGraph {
     public Vertex addVertex(final Object... keyValues) {
         ElementHelper.legalPropertyKeyValueArray(keyValues);
         Object idValue = 
vertexIdManager.convert(ElementHelper.getIdValue(keyValues).orElse(null));
-        final Set<String> labels = 
ElementHelper.getLabelsValue(keyValues).orElse(
-                Collections.singleton(defaultVertexLabel));
+        final Set<String> labels = 
ElementHelper.getLabelsValue(keyValues).orElse(null);
 
         if (null != idValue) {
             if (this.vertices.containsKey(idValue))
diff --git 
a/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerTransactionGraph.java
 
b/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerTransactionGraph.java
index 48bfc7a3de..3f1b37358c 100644
--- 
a/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerTransactionGraph.java
+++ 
b/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerTransactionGraph.java
@@ -87,9 +87,8 @@ public final class TinkerTransactionGraph extends 
AbstractTinkerGraph {
         allowNullPropertyValues = 
configuration.getBoolean(GREMLIN_TINKERGRAPH_ALLOW_NULL_PROPERTY_VALUES, false);
 
         vertexLabelCardinality = LabelCardinality.valueOf(
-                
configuration.getString(GREMLIN_TINKERGRAPH_VERTEX_LABEL_CARDINALITY, 
LabelCardinality.ZERO_OR_MORE.name()));
-        edgeLabelCardinality = LabelCardinality.valueOf(
-                
configuration.getString(GREMLIN_TINKERGRAPH_EDGE_LABEL_CARDINALITY, 
LabelCardinality.ZERO_OR_ONE.name()));
+                
configuration.getString(GREMLIN_TINKERGRAPH_VERTEX_LABEL_CARDINALITY, 
LabelCardinality.ONE.name()));
+        edgeLabelCardinality = LabelCardinality.ONE;
         defaultVertexLabel = Vertex.DEFAULT_LABEL;
         defaultEdgeLabel = Edge.DEFAULT_LABEL;
 
@@ -145,8 +144,7 @@ public final class TinkerTransactionGraph extends 
AbstractTinkerGraph {
         Object idValue = 
vertexIdManager.convert(ElementHelper.getIdValue(keyValues).orElse(null));
         if (null == idValue)
             idValue = vertexIdManager.getNextId(this);
-        final Set<String> labels = 
ElementHelper.getLabelsValue(keyValues).orElse(
-                Collections.singleton(defaultVertexLabel));
+        final Set<String> labels = 
ElementHelper.getLabelsValue(keyValues).orElse(null);
 
         this.tx().readWrite();
         final long txNumber = transaction.getTxNumber();
diff --git 
a/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerVertex.java
 
b/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerVertex.java
index c605841fd0..7896c3ad1a 100644
--- 
a/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerVertex.java
+++ 
b/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerVertex.java
@@ -78,34 +78,43 @@ public class TinkerVertex extends TinkerElement implements 
Vertex {
 
     /**
      * Canonical constructor. Constructs a TinkerVertex with multiple labels 
and a specific version (for transactional graphs).
+     * Uses a single-switch pattern to handle default label injection per the 
configured cardinality.
      */
     protected TinkerVertex(final Object id, final Set<String> labels, final 
AbstractTinkerGraph graph, final long currentVersion) {
         super(id, null, currentVersion);  // label field set below
         this.graph = graph;
         this.isTxMode = graph instanceof TinkerTransactionGraph;
         this.allowNullPropertyValues = 
graph.features().vertex().supportsNullPropertyValues();
-        // Store only explicit labels — never store the default label 
physically
-        this.vertexLabels = (labels == null) ? new HashSet<>() : new 
HashSet<>(labels);
-        // Set the cached label field to the effective value (including 
virtual default)
-        this.label = effectiveLabel();
-    }
 
-    /**
-     * Computes the effective single label for the deprecated label() API and 
the cached field.
-     */
-    private String effectiveLabel() {
-        if (this.vertexLabels.isEmpty()) {
-            return this.graph.vertexLabelCardinality.supportsZeroLabels() ? 
null : this.graph.defaultVertexLabel;
+        this.vertexLabels = new HashSet<>();
+        switch (graph.vertexLabelCardinality) {
+            case ONE:
+                // Exactly 1 label: use provided or default
+                if (labels != null && !labels.isEmpty())
+                    this.vertexLabels.addAll(labels);
+                else
+                    this.vertexLabels.add(graph.defaultVertexLabel);
+                break;
+            case ONE_OR_MORE:
+                // Default label always present alongside any user labels
+                this.vertexLabels.add(graph.defaultVertexLabel);
+                if (labels != null) this.vertexLabels.addAll(labels);
+                break;
+            case ZERO_OR_MORE:
+                // Fully flexible — store whatever was provided
+                if (labels != null) this.vertexLabels.addAll(labels);
+                break;
         }
-        return this.vertexLabels.iterator().next();
+
+        // Validate the resulting set conforms to cardinality
+        graph.vertexLabelCardinality.validateCreation(this.vertexLabels);
+
+        this.label = this.vertexLabels.isEmpty() ? "" : 
this.vertexLabels.iterator().next();
     }
 
     @Override
     public Set<String> labels() {
         if (this.vertexLabels.isEmpty()) {
-            if (!this.graph.vertexLabelCardinality.supportsZeroLabels()) {
-                return Collections.singleton(this.graph.defaultVertexLabel);
-            }
             return Collections.emptySet();
         }
         return Collections.unmodifiableSet(this.vertexLabels);
@@ -114,7 +123,10 @@ public class TinkerVertex extends TinkerElement implements 
Vertex {
     @Override
     @Deprecated
     public String label() {
-        return effectiveLabel();
+        if (this.vertexLabels.isEmpty()) {
+            return "";
+        }
+        return this.vertexLabels.iterator().next();
     }
 
     @Override
@@ -123,34 +135,32 @@ public class TinkerVertex extends TinkerElement 
implements Vertex {
         for (final String l : labels) {
             ElementHelper.validateLabel(l);
         }
-
         this.graph.vertexLabelCardinality.validateAdd(this.vertexLabels, 
label, labels);
-
+        graph.touch(this);
         this.vertexLabels.add(label);
         Collections.addAll(this.vertexLabels, labels);
-        this.label = effectiveLabel();
+        this.label = this.vertexLabels.iterator().next();
         this.graph.updateVertexLabelIndex(this);
     }
 
     @Override
     public void dropLabels() {
         this.graph.vertexLabelCardinality.validateDropAll(this.vertexLabels);
-
+        graph.touch(this);
         this.vertexLabels.clear();
-        this.label = effectiveLabel();
+        this.label = "";
         this.graph.updateVertexLabelIndex(this);
     }
 
     @Override
     public void dropLabel(final String label, final String... labels) {
-        this.graph.vertexLabelCardinality.validateDrop(this.vertexLabels, 
label);
-
+        this.graph.vertexLabelCardinality.validateDrop(this.vertexLabels, 
label, labels);
+        graph.touch(this);
         this.vertexLabels.remove(label);
         for (final String l : labels) {
             this.vertexLabels.remove(l);
         }
-
-        this.label = effectiveLabel();
+        this.label = this.vertexLabels.isEmpty() ? "" : 
this.vertexLabels.iterator().next();
         this.graph.updateVertexLabelIndex(this);
     }
 
diff --git 
a/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/TinkerWorld.java
 
b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/TinkerWorld.java
index 1a6ff95e02..1b5b6518b8 100644
--- 
a/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/TinkerWorld.java
+++ 
b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/TinkerWorld.java
@@ -65,6 +65,7 @@ public abstract class TinkerWorld implements World {
         conf.setProperty(TinkerGraph.GREMLIN_TINKERGRAPH_VERTEX_ID_MANAGER, 
AbstractTinkerGraph.DefaultIdManager.INTEGER.name());
         conf.setProperty(TinkerGraph.GREMLIN_TINKERGRAPH_EDGE_ID_MANAGER, 
AbstractTinkerGraph.DefaultIdManager.INTEGER.name());
         
conf.setProperty(TinkerGraph.GREMLIN_TINKERGRAPH_VERTEX_PROPERTY_ID_MANAGER, 
AbstractTinkerGraph.DefaultIdManager.LONG.name());
+        
conf.setProperty(AbstractTinkerGraph.GREMLIN_TINKERGRAPH_VERTEX_LABEL_CARDINALITY,
 "ZERO_OR_MORE");
         return conf;
     }
 
diff --git 
a/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/process/traversal/step/map/LabelsStepTest.java
 
b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/process/traversal/step/map/LabelsStepTest.java
index 9c816cf9c8..c357dd6bdb 100644
--- 
a/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/process/traversal/step/map/LabelsStepTest.java
+++ 
b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/process/traversal/step/map/LabelsStepTest.java
@@ -22,6 +22,7 @@ import 
org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSo
 import org.apache.tinkerpop.gremlin.structure.Edge;
 import org.apache.tinkerpop.gremlin.structure.Graph;
 import org.apache.tinkerpop.gremlin.structure.Vertex;
+import org.apache.tinkerpop.gremlin.tinkergraph.structure.AbstractTinkerGraph;
 import org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerGraph;
 import org.junit.After;
 import org.junit.Before;
@@ -46,7 +47,10 @@ public class LabelsStepTest {
 
     @Before
     public void setup() {
-        graph = TinkerGraph.open();
+        final org.apache.commons.configuration2.Configuration config = new 
org.apache.commons.configuration2.BaseConfiguration();
+        config.setProperty(Graph.GRAPH, TinkerGraph.class.getName());
+        
config.setProperty(AbstractTinkerGraph.GREMLIN_TINKERGRAPH_VERTEX_LABEL_CARDINALITY,
 "ZERO_OR_MORE");
+        graph = TinkerGraph.open(config);
         g = graph.traversal();
     }
 
@@ -100,7 +104,7 @@ public class LabelsStepTest {
     public void shouldStreamDefaultLabelFromDefaultVertex() {
         final Vertex v = g.addV().next();
         final List<String> labels = g.V(v).labels().toList();
-        assertThat(labels, hasSize(1));
-        assertThat(labels, containsInAnyOrder(Vertex.DEFAULT_LABEL));
+        // Under ZERO_OR_MORE cardinality, addV() with no label produces an 
empty label set
+        assertThat(labels, hasSize(0));
     }
 }
diff --git 
a/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/process/traversal/step/map/MergeVMultiLabelTest.java
 
b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/process/traversal/step/map/MergeVMultiLabelTest.java
index 847d79a077..c64996ee35 100644
--- 
a/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/process/traversal/step/map/MergeVMultiLabelTest.java
+++ 
b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/process/traversal/step/map/MergeVMultiLabelTest.java
@@ -23,6 +23,7 @@ import 
org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSo
 import org.apache.tinkerpop.gremlin.structure.Graph;
 import org.apache.tinkerpop.gremlin.structure.T;
 import org.apache.tinkerpop.gremlin.structure.Vertex;
+import org.apache.tinkerpop.gremlin.tinkergraph.structure.AbstractTinkerGraph;
 import org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerGraph;
 import org.junit.After;
 import org.junit.Before;
@@ -50,7 +51,10 @@ public class MergeVMultiLabelTest {
 
     @Before
     public void setup() {
-        graph = TinkerGraph.open();
+        final org.apache.commons.configuration2.Configuration config = new 
org.apache.commons.configuration2.BaseConfiguration();
+        config.setProperty(Graph.GRAPH, TinkerGraph.class.getName());
+        
config.setProperty(AbstractTinkerGraph.GREMLIN_TINKERGRAPH_VERTEX_LABEL_CARDINALITY,
 "ZERO_OR_MORE");
+        graph = TinkerGraph.open(config);
         g = graph.traversal();
     }
 
diff --git 
a/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/process/traversal/step/sideEffect/LabelMutationPropertyTest.java
 
b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/process/traversal/step/sideEffect/LabelMutationPropertyTest.java
index 71ffc0ce94..8fd2245842 100644
--- 
a/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/process/traversal/step/sideEffect/LabelMutationPropertyTest.java
+++ 
b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/process/traversal/step/sideEffect/LabelMutationPropertyTest.java
@@ -23,6 +23,7 @@ import 
org.apache.tinkerpop.gremlin.process.traversal.step.util.WithOptions;
 import org.apache.tinkerpop.gremlin.structure.Graph;
 import org.apache.tinkerpop.gremlin.structure.T;
 import org.apache.tinkerpop.gremlin.structure.Vertex;
+import org.apache.tinkerpop.gremlin.tinkergraph.structure.AbstractTinkerGraph;
 import org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerGraph;
 import org.junit.After;
 import org.junit.Before;
@@ -59,7 +60,10 @@ public class LabelMutationPropertyTest {
 
     @Before
     public void setup() {
-        graph = TinkerGraph.open();
+        final org.apache.commons.configuration2.Configuration config = new 
org.apache.commons.configuration2.BaseConfiguration();
+        config.setProperty(Graph.GRAPH, TinkerGraph.class.getName());
+        
config.setProperty(AbstractTinkerGraph.GREMLIN_TINKERGRAPH_VERTEX_LABEL_CARDINALITY,
 "ZERO_OR_MORE");
+        graph = TinkerGraph.open(config);
         g = graph.traversal();
         random = new Random(42); // deterministic seed for reproducibility
     }
diff --git 
a/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/process/traversal/step/sideEffect/LabelMutationStepTest.java
 
b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/process/traversal/step/sideEffect/LabelMutationStepTest.java
index d9c00fbfc6..e5cadf5e42 100644
--- 
a/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/process/traversal/step/sideEffect/LabelMutationStepTest.java
+++ 
b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/process/traversal/step/sideEffect/LabelMutationStepTest.java
@@ -24,6 +24,7 @@ import org.apache.tinkerpop.gremlin.structure.Edge;
 import org.apache.tinkerpop.gremlin.structure.Graph;
 import org.apache.tinkerpop.gremlin.structure.T;
 import org.apache.tinkerpop.gremlin.structure.Vertex;
+import org.apache.tinkerpop.gremlin.tinkergraph.structure.AbstractTinkerGraph;
 import org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerGraph;
 import org.junit.After;
 import org.junit.Before;
@@ -51,7 +52,10 @@ public class LabelMutationStepTest {
 
     @Before
     public void setup() {
-        graph = TinkerGraph.open();
+        final org.apache.commons.configuration2.Configuration config = new 
org.apache.commons.configuration2.BaseConfiguration();
+        config.setProperty(Graph.GRAPH, TinkerGraph.class.getName());
+        
config.setProperty(AbstractTinkerGraph.GREMLIN_TINKERGRAPH_VERTEX_LABEL_CARDINALITY,
 "ZERO_OR_MORE");
+        graph = TinkerGraph.open(config);
         g = graph.traversal();
     }
 
@@ -113,14 +117,13 @@ public class LabelMutationStepTest {
         assertThat(v.labels(), containsInAnyOrder("employee"));
     }
 
-    @Test
+    @Test(expected = IllegalStateException.class)
     public void shouldDropLabelsOnEdgeViaTraversal() {
         final Vertex v1 = g.addV("person").next();
         final Vertex v2 = g.addV("person").next();
         final Edge e = v1.addEdge("knows", v2);
+        // Edge cardinality is always ONE — dropLabels() throws
         g.E().dropLabels().iterate();
-        // Under ZERO_OR_ONE cardinality with supportsZeroLabels, edge labels 
become empty
-        assertThat(e.labels(), hasSize(0));
     }
 
     // --- addV multi-label tests ---
@@ -135,8 +138,8 @@ public class LabelMutationStepTest {
     @Test
     public void shouldCreateVertexWithDefaultLabelViaAddV() {
         final Vertex v = g.addV().next();
-        assertThat(v.labels(), hasSize(1));
-        assertThat(v.labels(), containsInAnyOrder(Vertex.DEFAULT_LABEL));
+        // Under ZERO_OR_MORE cardinality, addV() with no label produces an 
empty label set
+        assertThat(v.labels(), hasSize(0));
     }
 
     // --- hasLabel index consistency tests ---
diff --git 
a/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/LabelCardinalityTest.java
 
b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/LabelCardinalityTest.java
new file mode 100644
index 0000000000..f3555f31de
--- /dev/null
+++ 
b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/LabelCardinalityTest.java
@@ -0,0 +1,396 @@
+/*
+ * 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.
+ */
+package org.apache.tinkerpop.gremlin.tinkergraph.structure;
+
+import org.apache.commons.configuration2.BaseConfiguration;
+import org.apache.commons.configuration2.Configuration;
+import 
org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource;
+import org.apache.tinkerpop.gremlin.structure.Graph;
+import org.apache.tinkerpop.gremlin.structure.Vertex;
+import org.junit.Test;
+
+import java.util.Set;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsInAnyOrder;
+import static org.hamcrest.Matchers.empty;
+import static org.hamcrest.Matchers.hasSize;
+
+/**
+ * Tests for vertex label cardinality modes: ONE, ONE_OR_MORE, and 
ZERO_OR_MORE.
+ */
+public class LabelCardinalityTest {
+
+    // ========================================================================
+    // ONE mode (TinkerGraph default)
+    // ========================================================================
+
+    @Test
+    public void oneMode_addVNoLabel_shouldDefaultToVertex() throws Exception {
+        final Graph graph = TinkerGraph.open();
+        try {
+            final GraphTraversalSource g = graph.traversal();
+            final Vertex v = g.addV().next();
+            assertThat(v.labels(), hasSize(1));
+            assertThat(v.labels(), containsInAnyOrder(Vertex.DEFAULT_LABEL));
+        } finally {
+            graph.close();
+        }
+    }
+
+    @Test
+    public void oneMode_addVWithLabel_shouldHaveThatLabel() throws Exception {
+        final Graph graph = TinkerGraph.open();
+        try {
+            final GraphTraversalSource g = graph.traversal();
+            final Vertex v = g.addV("person").next();
+            assertThat(v.labels(), hasSize(1));
+            assertThat(v.labels(), containsInAnyOrder("person"));
+        } finally {
+            graph.close();
+        }
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void oneMode_addVWithMultipleLabels_shouldThrow() throws Exception {
+        final Graph graph = TinkerGraph.open();
+        try {
+            final GraphTraversalSource g = graph.traversal();
+            g.addV("a", "b").next();
+        } finally {
+            graph.close();
+        }
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void oneMode_addLabel_shouldThrow() throws Exception {
+        final Graph graph = TinkerGraph.open();
+        try {
+            final GraphTraversalSource g = graph.traversal();
+            final Vertex v = g.addV("person").next();
+            v.addLabel("x");
+        } finally {
+            graph.close();
+        }
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void oneMode_dropLabel_shouldThrow() throws Exception {
+        final Graph graph = TinkerGraph.open();
+        try {
+            final GraphTraversalSource g = graph.traversal();
+            final Vertex v = g.addV("person").next();
+            v.dropLabel(Vertex.DEFAULT_LABEL);
+        } finally {
+            graph.close();
+        }
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void oneMode_dropLabels_shouldThrow() throws Exception {
+        final Graph graph = TinkerGraph.open();
+        try {
+            final GraphTraversalSource g = graph.traversal();
+            final Vertex v = g.addV("person").next();
+            v.dropLabels();
+        } finally {
+            graph.close();
+        }
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void oneMode_addLabelViaTraversal_shouldThrow() throws Exception {
+        final Graph graph = TinkerGraph.open();
+        try {
+            final GraphTraversalSource g = graph.traversal();
+            g.addV("person").addLabel("x").iterate();
+        } finally {
+            graph.close();
+        }
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void oneMode_dropLabelViaTraversal_shouldThrow() throws Exception {
+        final Graph graph = TinkerGraph.open();
+        try {
+            final GraphTraversalSource g = graph.traversal();
+            g.addV("person").dropLabel("person").iterate();
+        } finally {
+            graph.close();
+        }
+    }
+
+    // ========================================================================
+    // ONE_OR_MORE mode
+    // ========================================================================
+
+    private Graph openOneOrMore() {
+        final Configuration config = new BaseConfiguration();
+        config.setProperty(Graph.GRAPH, TinkerGraph.class.getName());
+        
config.setProperty(AbstractTinkerGraph.GREMLIN_TINKERGRAPH_VERTEX_LABEL_CARDINALITY,
 "ONE_OR_MORE");
+        return TinkerGraph.open(config);
+    }
+
+    @Test
+    public void oneOrMoreMode_addVNoLabel_shouldContainDefault() throws 
Exception {
+        final Graph graph = openOneOrMore();
+        try {
+            final GraphTraversalSource g = graph.traversal();
+            final Vertex v = g.addV().next();
+            assertThat(v.labels(), containsInAnyOrder(Vertex.DEFAULT_LABEL));
+        } finally {
+            graph.close();
+        }
+    }
+
+    @Test
+    public void oneOrMoreMode_addVWithLabel_shouldContainDefaultAndUserLabel() 
throws Exception {
+        final Graph graph = openOneOrMore();
+        try {
+            final GraphTraversalSource g = graph.traversal();
+            final Vertex v = g.addV("person").next();
+            assertThat(v.labels(), containsInAnyOrder(Vertex.DEFAULT_LABEL, 
"person"));
+        } finally {
+            graph.close();
+        }
+    }
+
+    @Test
+    public void oneOrMoreMode_addLabel_shouldAccumulate() throws Exception {
+        final Graph graph = openOneOrMore();
+        try {
+            final GraphTraversalSource g = graph.traversal();
+            final Vertex v = g.addV("person").next();
+            v.addLabel("employee");
+            assertThat(v.labels(), containsInAnyOrder(Vertex.DEFAULT_LABEL, 
"person", "employee"));
+        } finally {
+            graph.close();
+        }
+    }
+
+    @Test
+    public void oneOrMoreMode_dropLabel_shouldSucceedIfAtLeastOneRemains() 
throws Exception {
+        final Graph graph = openOneOrMore();
+        try {
+            final GraphTraversalSource g = graph.traversal();
+            final Vertex v = g.addV("person").next();
+            // Has {"vertex", "person"} — dropping "person" leaves {"vertex"}
+            v.dropLabel("person");
+            assertThat(v.labels(), containsInAnyOrder(Vertex.DEFAULT_LABEL));
+        } finally {
+            graph.close();
+        }
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void oneOrMoreMode_dropLabel_shouldThrowIfWouldLeaveZero() throws 
Exception {
+        final Graph graph = openOneOrMore();
+        try {
+            final GraphTraversalSource g = graph.traversal();
+            final Vertex v = g.addV().next();
+            // Has {"vertex"} — dropping "vertex" would leave 0
+            v.dropLabel(Vertex.DEFAULT_LABEL);
+        } finally {
+            graph.close();
+        }
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void oneOrMoreMode_dropMultipleLabels_shouldThrowIfWouldLeaveZero() 
throws Exception {
+        final Graph graph = openOneOrMore();
+        try {
+            final GraphTraversalSource g = graph.traversal();
+            final Vertex v = g.addV("person").next();
+            // Has {"vertex", "person"} — dropping both would leave 0
+            v.dropLabel("person", Vertex.DEFAULT_LABEL);
+        } finally {
+            graph.close();
+        }
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void oneOrMoreMode_dropLabels_shouldAlwaysThrow() throws Exception {
+        final Graph graph = openOneOrMore();
+        try {
+            final GraphTraversalSource g = graph.traversal();
+            final Vertex v = g.addV("person").next();
+            v.dropLabels();
+        } finally {
+            graph.close();
+        }
+    }
+
+    @Test
+    public void oneOrMoreMode_addVWithMultipleLabels_shouldIncludeDefault() 
throws Exception {
+        final Graph graph = openOneOrMore();
+        try {
+            final GraphTraversalSource g = graph.traversal();
+            final Vertex v = g.addV("a", "b").next();
+            assertThat(v.labels(), containsInAnyOrder(Vertex.DEFAULT_LABEL, 
"a", "b"));
+        } finally {
+            graph.close();
+        }
+    }
+
+    @Test
+    public void oneOrMoreMode_addDuplicateLabel_shouldBeNoOp() throws 
Exception {
+        final Graph graph = openOneOrMore();
+        try {
+            final GraphTraversalSource g = graph.traversal();
+            final Vertex v = g.addV("person").next();
+            // Has {"vertex", "person"}. Adding "person" again should be no-op.
+            v.addLabel("person");
+            assertThat(v.labels(), containsInAnyOrder(Vertex.DEFAULT_LABEL, 
"person"));
+            assertThat(v.labels(), hasSize(2));
+        } finally {
+            graph.close();
+        }
+    }
+
+    @Test
+    public void oneOrMoreMode_dropNonExistentLabel_shouldBeNoOp() throws 
Exception {
+        final Graph graph = openOneOrMore();
+        try {
+            final GraphTraversalSource g = graph.traversal();
+            final Vertex v = g.addV("person").next();
+            // Has {"vertex", "person"}. Dropping "nonexistent" should be 
no-op.
+            v.dropLabel("nonexistent");
+            assertThat(v.labels(), containsInAnyOrder(Vertex.DEFAULT_LABEL, 
"person"));
+            assertThat(v.labels(), hasSize(2));
+        } finally {
+            graph.close();
+        }
+    }
+
+    // ========================================================================
+    // ZERO_OR_MORE mode
+    // ========================================================================
+
+    private Graph openZeroOrMore() {
+        final Configuration config = new BaseConfiguration();
+        config.setProperty(Graph.GRAPH, TinkerGraph.class.getName());
+        
config.setProperty(AbstractTinkerGraph.GREMLIN_TINKERGRAPH_VERTEX_LABEL_CARDINALITY,
 "ZERO_OR_MORE");
+        return TinkerGraph.open(config);
+    }
+
+    @Test
+    public void zeroOrMoreMode_addVNoLabel_shouldBeEmpty() throws Exception {
+        final Graph graph = openZeroOrMore();
+        try {
+            final GraphTraversalSource g = graph.traversal();
+            final Vertex v = g.addV().next();
+            assertThat(v.labels(), is(empty()));
+        } finally {
+            graph.close();
+        }
+    }
+
+    @Test
+    public void zeroOrMoreMode_addVWithLabel_shouldHaveOnlyThatLabel() throws 
Exception {
+        final Graph graph = openZeroOrMore();
+        try {
+            final GraphTraversalSource g = graph.traversal();
+            final Vertex v = g.addV("person").next();
+            assertThat(v.labels(), hasSize(1));
+            assertThat(v.labels(), containsInAnyOrder("person"));
+        } finally {
+            graph.close();
+        }
+    }
+
+    @Test
+    public void zeroOrMoreMode_addLabel_shouldSucceed() throws Exception {
+        final Graph graph = openZeroOrMore();
+        try {
+            final GraphTraversalSource g = graph.traversal();
+            final Vertex v = g.addV("person").next();
+            v.addLabel("x");
+            assertThat(v.labels(), hasSize(2));
+            assertThat(v.labels(), containsInAnyOrder("person", "x"));
+        } finally {
+            graph.close();
+        }
+    }
+
+    @Test
+    public void zeroOrMoreMode_dropLabel_shouldAllowEmptyResult() throws 
Exception {
+        final Graph graph = openZeroOrMore();
+        try {
+            final GraphTraversalSource g = graph.traversal();
+            final Vertex v = g.addV("person").next();
+            v.dropLabel("person");
+            assertThat(v.labels(), is(empty()));
+        } finally {
+            graph.close();
+        }
+    }
+
+    @Test
+    public void zeroOrMoreMode_dropLabels_shouldResultInEmpty() throws 
Exception {
+        final Graph graph = openZeroOrMore();
+        try {
+            final GraphTraversalSource g = graph.traversal();
+            final Vertex v = g.addV("person").addLabel("employee").next();
+            v.dropLabels();
+            assertThat(v.labels(), is(empty()));
+        } finally {
+            graph.close();
+        }
+    }
+
+    @Test
+    public void 
zeroOrMoreMode_addVWithMultipleLabels_shouldHaveOnlyThoseLabels() throws 
Exception {
+        final Graph graph = openZeroOrMore();
+        try {
+            final GraphTraversalSource g = graph.traversal();
+            final Vertex v = g.addV("a", "b").next();
+            assertThat(v.labels(), hasSize(2));
+            assertThat(v.labels(), containsInAnyOrder("a", "b"));
+        } finally {
+            graph.close();
+        }
+    }
+
+    @Test
+    public void 
zeroOrMoreMode_labelSingular_shouldReturnEmptyStringForNoLabels() throws 
Exception {
+        final Graph graph = openZeroOrMore();
+        try {
+            final GraphTraversalSource g = graph.traversal();
+            final Vertex v = g.addV().next();
+            assertThat(v.labels(), is(empty()));
+            // label() (singular, deprecated) should return "" not null
+            assertThat(v.label(), isString(""));
+        } finally {
+            graph.close();
+        }
+    }
+
+    // helper for static import of Matchers.is and empty
+    private static org.hamcrest.Matcher<java.util.Collection<?>> 
is(org.hamcrest.Matcher<java.util.Collection<?>> matcher) {
+        return org.hamcrest.Matchers.is(matcher);
+    }
+
+    private static org.hamcrest.Matcher<java.util.Collection<?>> empty() {
+        return org.hamcrest.Matchers.empty();
+    }
+
+    private static org.hamcrest.Matcher<String> isString(final String 
expected) {
+        return org.hamcrest.Matchers.is(expected);
+    }
+}
diff --git 
a/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/LabelReplacePatternValidationTest.java
 
b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/LabelReplacePatternValidationTest.java
index 8e81da7837..ca296dd08a 100644
--- 
a/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/LabelReplacePatternValidationTest.java
+++ 
b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/LabelReplacePatternValidationTest.java
@@ -24,6 +24,8 @@ import 
org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__;
 import org.apache.tinkerpop.gremlin.structure.Graph;
 import org.apache.tinkerpop.gremlin.structure.T;
 import org.apache.tinkerpop.gremlin.structure.Vertex;
+import org.apache.tinkerpop.gremlin.tinkergraph.structure.AbstractTinkerGraph;
+import org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerGraph;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
@@ -54,7 +56,10 @@ public class LabelReplacePatternValidationTest {
 
     @Before
     public void setup() {
-        graph = TinkerGraph.open();
+        final org.apache.commons.configuration2.Configuration config = new 
org.apache.commons.configuration2.BaseConfiguration();
+        config.setProperty(Graph.GRAPH, TinkerGraph.class.getName());
+        
config.setProperty(AbstractTinkerGraph.GREMLIN_TINKERGRAPH_VERTEX_LABEL_CARDINALITY,
 "ZERO_OR_MORE");
+        graph = TinkerGraph.open(config);
         g = graph.traversal();
     }
 
diff --git 
a/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/MergeOnMatchLabelPatternsTest.java
 
b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/MergeOnMatchLabelPatternsTest.java
index 680f442bcf..0a68dbdc1d 100644
--- 
a/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/MergeOnMatchLabelPatternsTest.java
+++ 
b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/MergeOnMatchLabelPatternsTest.java
@@ -23,6 +23,8 @@ import 
org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSo
 import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__;
 import org.apache.tinkerpop.gremlin.structure.Graph;
 import org.apache.tinkerpop.gremlin.structure.T;
+import org.apache.tinkerpop.gremlin.tinkergraph.structure.AbstractTinkerGraph;
+import org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerGraph;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
@@ -47,7 +49,10 @@ public class MergeOnMatchLabelPatternsTest {
 
     @Before
     public void setup() {
-        graph = TinkerGraph.open();
+        final org.apache.commons.configuration2.Configuration config = new 
org.apache.commons.configuration2.BaseConfiguration();
+        config.setProperty(Graph.GRAPH, TinkerGraph.class.getName());
+        
config.setProperty(AbstractTinkerGraph.GREMLIN_TINKERGRAPH_VERTEX_LABEL_CARDINALITY,
 "ZERO_OR_MORE");
+        graph = TinkerGraph.open(config);
         g = graph.traversal();
     }
 
diff --git 
a/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerVertexMultiLabelGremlinLangTest.java
 
b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerVertexMultiLabelGremlinLangTest.java
index 99d373bcdd..933be74f92 100644
--- 
a/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerVertexMultiLabelGremlinLangTest.java
+++ 
b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerVertexMultiLabelGremlinLangTest.java
@@ -23,6 +23,8 @@ import 
org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversal;
 import 
org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource;
 import org.apache.tinkerpop.gremlin.structure.Graph;
 import org.apache.tinkerpop.gremlin.structure.Vertex;
+import org.apache.tinkerpop.gremlin.tinkergraph.structure.AbstractTinkerGraph;
+import org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerGraph;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
@@ -48,7 +50,10 @@ public class TinkerVertexMultiLabelGremlinLangTest {
 
     @Before
     public void setup() {
-        graph = TinkerGraph.open();
+        final org.apache.commons.configuration2.Configuration config = new 
org.apache.commons.configuration2.BaseConfiguration();
+        config.setProperty(Graph.GRAPH, TinkerGraph.class.getName());
+        
config.setProperty(AbstractTinkerGraph.GREMLIN_TINKERGRAPH_VERTEX_LABEL_CARDINALITY,
 "ZERO_OR_MORE");
+        graph = TinkerGraph.open(config);
         g = graph.traversal();
     }
 
diff --git 
a/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerVertexMultiLabelTest.java
 
b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerVertexMultiLabelTest.java
index ffd5efb697..e9da6dc2bf 100644
--- 
a/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerVertexMultiLabelTest.java
+++ 
b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerVertexMultiLabelTest.java
@@ -22,6 +22,8 @@ import 
org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSo
 import org.apache.tinkerpop.gremlin.structure.Edge;
 import org.apache.tinkerpop.gremlin.structure.Graph;
 import org.apache.tinkerpop.gremlin.structure.Vertex;
+import org.apache.tinkerpop.gremlin.tinkergraph.structure.AbstractTinkerGraph;
+import org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerGraph;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
@@ -43,7 +45,10 @@ public class TinkerVertexMultiLabelTest {
 
     @Before
     public void setup() {
-        graph = TinkerGraph.open();
+        final org.apache.commons.configuration2.Configuration config = new 
org.apache.commons.configuration2.BaseConfiguration();
+        config.setProperty(Graph.GRAPH, TinkerGraph.class.getName());
+        
config.setProperty(AbstractTinkerGraph.GREMLIN_TINKERGRAPH_VERTEX_LABEL_CARDINALITY,
 "ZERO_OR_MORE");
+        graph = TinkerGraph.open(config);
         g = graph.traversal();
     }
 
@@ -69,8 +74,8 @@ public class TinkerVertexMultiLabelTest {
     @Test
     public void shouldCreateVertexWithDefaultLabelWhenNoneSpecified() {
         final Vertex v = g.addV().next();
-        assertThat(v.labels(), hasSize(1));
-        assertThat(v.labels(), containsInAnyOrder(Vertex.DEFAULT_LABEL));
+        // Under ZERO_OR_MORE cardinality, addV() with no label produces an 
empty label set
+        assertThat(v.labels(), hasSize(0));
     }
 
     @SuppressWarnings("deprecation")
@@ -156,14 +161,13 @@ public class TinkerVertexMultiLabelTest {
         e.addLabel("friend");
     }
 
-    @Test
+    @Test(expected = IllegalStateException.class)
     public void shouldDropLabelsOnEdge() {
         final Vertex v1 = g.addV("person").next();
         final Vertex v2 = g.addV("person").next();
         final Edge e = v1.addEdge("knows", v2);
+        // Edge cardinality is always ONE — dropLabels() throws
         e.dropLabels();
-        // Under ZERO_OR_ONE cardinality for edges with supportsZeroLabels, 
labels() returns empty set
-        assertThat(e.labels(), hasSize(0));
     }
 
     @Test
@@ -177,12 +181,12 @@ public class TinkerVertexMultiLabelTest {
 
     @Test
     public void shouldAddLabelToExistingVertexWithDefaultLabel() {
-        // Under ZERO_OR_MORE, addV() assigns "vertex" physically. Adding 
"person" simply adds to the set.
+        // Under ZERO_OR_MORE, addV() assigns no labels. Adding "person" 
creates a single-label set.
         final Vertex v = g.addV().next();
-        assertThat(v.labels(), containsInAnyOrder(Vertex.DEFAULT_LABEL));
+        assertThat(v.labels(), hasSize(0));
         v.addLabel("person");
-        assertThat(v.labels(), hasSize(2));
-        assertThat(v.labels(), containsInAnyOrder(Vertex.DEFAULT_LABEL, 
"person"));
+        assertThat(v.labels(), hasSize(1));
+        assertThat(v.labels(), containsInAnyOrder("person"));
     }
 
     @Test

Reply via email to