kubectl-mcp-server 1.16.0__py3-none-any.whl → 1.17.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {kubectl_mcp_server-1.16.0.dist-info → kubectl_mcp_server-1.17.0.dist-info}/METADATA +1 -1
- {kubectl_mcp_server-1.16.0.dist-info → kubectl_mcp_server-1.17.0.dist-info}/RECORD +28 -14
- kubectl_mcp_tool/__init__.py +1 -1
- kubectl_mcp_tool/cli/cli.py +83 -9
- kubectl_mcp_tool/cli/output.py +14 -0
- kubectl_mcp_tool/config/__init__.py +46 -0
- kubectl_mcp_tool/config/loader.py +386 -0
- kubectl_mcp_tool/config/schema.py +184 -0
- kubectl_mcp_tool/mcp_server.py +219 -8
- kubectl_mcp_tool/observability/__init__.py +59 -0
- kubectl_mcp_tool/observability/metrics.py +223 -0
- kubectl_mcp_tool/observability/stats.py +255 -0
- kubectl_mcp_tool/observability/tracing.py +335 -0
- kubectl_mcp_tool/prompts/__init__.py +43 -0
- kubectl_mcp_tool/prompts/builtin.py +695 -0
- kubectl_mcp_tool/prompts/custom.py +298 -0
- kubectl_mcp_tool/prompts/prompts.py +180 -4
- kubectl_mcp_tool/safety.py +155 -0
- kubectl_mcp_tool/tools/cluster.py +384 -0
- tests/test_config.py +386 -0
- tests/test_mcp_integration.py +251 -0
- tests/test_observability.py +521 -0
- tests/test_prompts.py +716 -0
- tests/test_safety.py +218 -0
- {kubectl_mcp_server-1.16.0.dist-info → kubectl_mcp_server-1.17.0.dist-info}/WHEEL +0 -0
- {kubectl_mcp_server-1.16.0.dist-info → kubectl_mcp_server-1.17.0.dist-info}/entry_points.txt +0 -0
- {kubectl_mcp_server-1.16.0.dist-info → kubectl_mcp_server-1.17.0.dist-info}/licenses/LICENSE +0 -0
- {kubectl_mcp_server-1.16.0.dist-info → kubectl_mcp_server-1.17.0.dist-info}/top_level.txt +0 -0
tests/test_prompts.py
CHANGED
|
@@ -10,11 +10,42 @@ This module tests all FastMCP 3 prompts including:
|
|
|
10
10
|
- debug_networking
|
|
11
11
|
- scale_application
|
|
12
12
|
- upgrade_cluster
|
|
13
|
+
|
|
14
|
+
Also tests the configurable prompts system:
|
|
15
|
+
- Custom prompt loading from TOML
|
|
16
|
+
- Template rendering with Mustache syntax
|
|
17
|
+
- Built-in prompts (cluster-health-check, debug-workload, etc.)
|
|
18
|
+
- Prompt validation and merging
|
|
13
19
|
"""
|
|
14
20
|
|
|
15
21
|
import pytest
|
|
22
|
+
import tempfile
|
|
23
|
+
import os
|
|
16
24
|
from unittest.mock import patch, MagicMock
|
|
17
25
|
|
|
26
|
+
from kubectl_mcp_tool.prompts.custom import (
|
|
27
|
+
CustomPrompt,
|
|
28
|
+
PromptArgument,
|
|
29
|
+
PromptMessage,
|
|
30
|
+
render_prompt,
|
|
31
|
+
load_prompts_from_config,
|
|
32
|
+
load_prompts_from_toml_file,
|
|
33
|
+
validate_prompt_args,
|
|
34
|
+
apply_defaults,
|
|
35
|
+
get_prompt_schema,
|
|
36
|
+
)
|
|
37
|
+
from kubectl_mcp_tool.prompts.builtin import (
|
|
38
|
+
BUILTIN_PROMPTS,
|
|
39
|
+
get_builtin_prompts,
|
|
40
|
+
get_builtin_prompt_by_name,
|
|
41
|
+
CLUSTER_HEALTH_CHECK,
|
|
42
|
+
DEBUG_WORKLOAD,
|
|
43
|
+
RESOURCE_USAGE,
|
|
44
|
+
SECURITY_POSTURE,
|
|
45
|
+
DEPLOYMENT_CHECKLIST,
|
|
46
|
+
INCIDENT_RESPONSE,
|
|
47
|
+
)
|
|
48
|
+
|
|
18
49
|
|
|
19
50
|
class TestTroubleshootWorkloadPrompt:
|
|
20
51
|
"""Tests for troubleshoot_workload prompt."""
|
|
@@ -534,3 +565,688 @@ class TestPromptContent:
|
|
|
534
565
|
for statement in action_statements:
|
|
535
566
|
# Verify each statement is a valid action prompt
|
|
536
567
|
assert "now" in statement.lower() or "begin" in statement.lower() or "start" in statement.lower()
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
# =============================================================================
|
|
571
|
+
# Configurable Prompts System Tests
|
|
572
|
+
# =============================================================================
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
class TestPromptArgument:
|
|
576
|
+
"""Tests for PromptArgument dataclass."""
|
|
577
|
+
|
|
578
|
+
@pytest.mark.unit
|
|
579
|
+
def test_create_required_argument(self):
|
|
580
|
+
"""Test creating a required prompt argument."""
|
|
581
|
+
arg = PromptArgument(
|
|
582
|
+
name="pod_name",
|
|
583
|
+
description="Name of the pod",
|
|
584
|
+
required=True
|
|
585
|
+
)
|
|
586
|
+
assert arg.name == "pod_name"
|
|
587
|
+
assert arg.description == "Name of the pod"
|
|
588
|
+
assert arg.required is True
|
|
589
|
+
assert arg.default == ""
|
|
590
|
+
|
|
591
|
+
@pytest.mark.unit
|
|
592
|
+
def test_create_optional_argument_with_default(self):
|
|
593
|
+
"""Test creating an optional argument with default value."""
|
|
594
|
+
arg = PromptArgument(
|
|
595
|
+
name="namespace",
|
|
596
|
+
description="Kubernetes namespace",
|
|
597
|
+
required=False,
|
|
598
|
+
default="default"
|
|
599
|
+
)
|
|
600
|
+
assert arg.name == "namespace"
|
|
601
|
+
assert arg.required is False
|
|
602
|
+
assert arg.default == "default"
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
class TestPromptMessage:
|
|
606
|
+
"""Tests for PromptMessage dataclass."""
|
|
607
|
+
|
|
608
|
+
@pytest.mark.unit
|
|
609
|
+
def test_create_user_message(self):
|
|
610
|
+
"""Test creating a user message."""
|
|
611
|
+
msg = PromptMessage(
|
|
612
|
+
role="user",
|
|
613
|
+
content="Debug the pod in namespace default"
|
|
614
|
+
)
|
|
615
|
+
assert msg.role == "user"
|
|
616
|
+
assert msg.content == "Debug the pod in namespace default"
|
|
617
|
+
|
|
618
|
+
@pytest.mark.unit
|
|
619
|
+
def test_create_assistant_message(self):
|
|
620
|
+
"""Test creating an assistant message."""
|
|
621
|
+
msg = PromptMessage(
|
|
622
|
+
role="assistant",
|
|
623
|
+
content="I'll help you debug the pod."
|
|
624
|
+
)
|
|
625
|
+
assert msg.role == "assistant"
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
class TestCustomPrompt:
|
|
629
|
+
"""Tests for CustomPrompt dataclass."""
|
|
630
|
+
|
|
631
|
+
@pytest.mark.unit
|
|
632
|
+
def test_create_simple_prompt(self):
|
|
633
|
+
"""Test creating a simple custom prompt."""
|
|
634
|
+
prompt = CustomPrompt(
|
|
635
|
+
name="test-prompt",
|
|
636
|
+
title="Test Prompt",
|
|
637
|
+
description="A test prompt"
|
|
638
|
+
)
|
|
639
|
+
assert prompt.name == "test-prompt"
|
|
640
|
+
assert prompt.title == "Test Prompt"
|
|
641
|
+
assert prompt.description == "A test prompt"
|
|
642
|
+
assert prompt.arguments == []
|
|
643
|
+
assert prompt.messages == []
|
|
644
|
+
|
|
645
|
+
@pytest.mark.unit
|
|
646
|
+
def test_create_prompt_with_arguments_and_messages(self):
|
|
647
|
+
"""Test creating a prompt with arguments and messages."""
|
|
648
|
+
prompt = CustomPrompt(
|
|
649
|
+
name="debug-pod",
|
|
650
|
+
title="Debug Pod",
|
|
651
|
+
description="Debug a Kubernetes pod",
|
|
652
|
+
arguments=[
|
|
653
|
+
PromptArgument(name="pod_name", required=True),
|
|
654
|
+
PromptArgument(name="namespace", default="default"),
|
|
655
|
+
],
|
|
656
|
+
messages=[
|
|
657
|
+
PromptMessage(role="user", content="Debug pod {{pod_name}}"),
|
|
658
|
+
]
|
|
659
|
+
)
|
|
660
|
+
assert len(prompt.arguments) == 2
|
|
661
|
+
assert len(prompt.messages) == 1
|
|
662
|
+
assert prompt.arguments[0].name == "pod_name"
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
class TestRenderPrompt:
|
|
666
|
+
"""Tests for render_prompt function."""
|
|
667
|
+
|
|
668
|
+
@pytest.mark.unit
|
|
669
|
+
def test_simple_variable_substitution(self):
|
|
670
|
+
"""Test basic variable substitution."""
|
|
671
|
+
prompt = CustomPrompt(
|
|
672
|
+
name="test",
|
|
673
|
+
description="Test",
|
|
674
|
+
messages=[
|
|
675
|
+
PromptMessage(role="user", content="Debug pod {{pod_name}} in {{namespace}}")
|
|
676
|
+
]
|
|
677
|
+
)
|
|
678
|
+
result = render_prompt(prompt, {"pod_name": "nginx", "namespace": "production"})
|
|
679
|
+
assert len(result) == 1
|
|
680
|
+
assert result[0].content == "Debug pod nginx in production"
|
|
681
|
+
assert result[0].role == "user"
|
|
682
|
+
|
|
683
|
+
@pytest.mark.unit
|
|
684
|
+
def test_conditional_section_shown_when_true(self):
|
|
685
|
+
"""Test conditional section is shown when variable is truthy."""
|
|
686
|
+
prompt = CustomPrompt(
|
|
687
|
+
name="test",
|
|
688
|
+
description="Test",
|
|
689
|
+
messages=[
|
|
690
|
+
PromptMessage(
|
|
691
|
+
role="user",
|
|
692
|
+
content="Check pods{{#namespace}} in namespace {{namespace}}{{/namespace}}."
|
|
693
|
+
)
|
|
694
|
+
]
|
|
695
|
+
)
|
|
696
|
+
result = render_prompt(prompt, {"namespace": "production"})
|
|
697
|
+
assert "in namespace production" in result[0].content
|
|
698
|
+
|
|
699
|
+
@pytest.mark.unit
|
|
700
|
+
def test_conditional_section_hidden_when_false(self):
|
|
701
|
+
"""Test conditional section is hidden when variable is falsy."""
|
|
702
|
+
prompt = CustomPrompt(
|
|
703
|
+
name="test",
|
|
704
|
+
description="Test",
|
|
705
|
+
messages=[
|
|
706
|
+
PromptMessage(
|
|
707
|
+
role="user",
|
|
708
|
+
content="Check pods{{#namespace}} in namespace {{namespace}}{{/namespace}}."
|
|
709
|
+
)
|
|
710
|
+
]
|
|
711
|
+
)
|
|
712
|
+
result = render_prompt(prompt, {})
|
|
713
|
+
assert "in namespace" not in result[0].content
|
|
714
|
+
assert result[0].content == "Check pods."
|
|
715
|
+
|
|
716
|
+
@pytest.mark.unit
|
|
717
|
+
def test_conditional_section_with_false_value(self):
|
|
718
|
+
"""Test conditional section is hidden when variable is 'false'."""
|
|
719
|
+
prompt = CustomPrompt(
|
|
720
|
+
name="test",
|
|
721
|
+
description="Test",
|
|
722
|
+
messages=[
|
|
723
|
+
PromptMessage(
|
|
724
|
+
role="user",
|
|
725
|
+
content="{{#check_metrics}}Include metrics analysis.{{/check_metrics}}"
|
|
726
|
+
)
|
|
727
|
+
]
|
|
728
|
+
)
|
|
729
|
+
result = render_prompt(prompt, {"check_metrics": "false"})
|
|
730
|
+
assert "Include metrics" not in result[0].content
|
|
731
|
+
|
|
732
|
+
@pytest.mark.unit
|
|
733
|
+
def test_inverse_section_shown_when_false(self):
|
|
734
|
+
"""Test inverse section is shown when variable is falsy."""
|
|
735
|
+
prompt = CustomPrompt(
|
|
736
|
+
name="test",
|
|
737
|
+
description="Test",
|
|
738
|
+
messages=[
|
|
739
|
+
PromptMessage(
|
|
740
|
+
role="user",
|
|
741
|
+
content="{{^namespace}}All namespaces{{/namespace}}{{#namespace}}Namespace: {{namespace}}{{/namespace}}"
|
|
742
|
+
)
|
|
743
|
+
]
|
|
744
|
+
)
|
|
745
|
+
result = render_prompt(prompt, {})
|
|
746
|
+
assert "All namespaces" in result[0].content
|
|
747
|
+
|
|
748
|
+
@pytest.mark.unit
|
|
749
|
+
def test_inverse_section_hidden_when_true(self):
|
|
750
|
+
"""Test inverse section is hidden when variable is truthy."""
|
|
751
|
+
prompt = CustomPrompt(
|
|
752
|
+
name="test",
|
|
753
|
+
description="Test",
|
|
754
|
+
messages=[
|
|
755
|
+
PromptMessage(
|
|
756
|
+
role="user",
|
|
757
|
+
content="{{^namespace}}All namespaces{{/namespace}}{{#namespace}}Namespace: {{namespace}}{{/namespace}}"
|
|
758
|
+
)
|
|
759
|
+
]
|
|
760
|
+
)
|
|
761
|
+
result = render_prompt(prompt, {"namespace": "production"})
|
|
762
|
+
assert "All namespaces" not in result[0].content
|
|
763
|
+
assert "Namespace: production" in result[0].content
|
|
764
|
+
|
|
765
|
+
@pytest.mark.unit
|
|
766
|
+
def test_multiple_messages(self):
|
|
767
|
+
"""Test rendering multiple messages."""
|
|
768
|
+
prompt = CustomPrompt(
|
|
769
|
+
name="test",
|
|
770
|
+
description="Test",
|
|
771
|
+
messages=[
|
|
772
|
+
PromptMessage(role="user", content="Hello {{name}}"),
|
|
773
|
+
PromptMessage(role="assistant", content="Hi there!"),
|
|
774
|
+
PromptMessage(role="user", content="Debug {{pod}}"),
|
|
775
|
+
]
|
|
776
|
+
)
|
|
777
|
+
result = render_prompt(prompt, {"name": "Admin", "pod": "nginx"})
|
|
778
|
+
assert len(result) == 3
|
|
779
|
+
assert result[0].content == "Hello Admin"
|
|
780
|
+
assert result[2].content == "Debug nginx"
|
|
781
|
+
|
|
782
|
+
@pytest.mark.unit
|
|
783
|
+
def test_unsubstituted_variables_removed(self):
|
|
784
|
+
"""Test that unsubstituted optional variables are removed."""
|
|
785
|
+
prompt = CustomPrompt(
|
|
786
|
+
name="test",
|
|
787
|
+
description="Test",
|
|
788
|
+
messages=[
|
|
789
|
+
PromptMessage(role="user", content="Check {{resource}} status {{unknown_var}}")
|
|
790
|
+
]
|
|
791
|
+
)
|
|
792
|
+
result = render_prompt(prompt, {"resource": "pods"})
|
|
793
|
+
assert result[0].content == "Check pods status"
|
|
794
|
+
|
|
795
|
+
|
|
796
|
+
class TestLoadPromptsFromConfig:
|
|
797
|
+
"""Tests for load_prompts_from_config function."""
|
|
798
|
+
|
|
799
|
+
@pytest.mark.unit
|
|
800
|
+
def test_load_simple_prompt(self):
|
|
801
|
+
"""Test loading a simple prompt from config dict."""
|
|
802
|
+
config = {
|
|
803
|
+
"prompts": [
|
|
804
|
+
{
|
|
805
|
+
"name": "debug-pod",
|
|
806
|
+
"title": "Debug Pod",
|
|
807
|
+
"description": "Debug a pod",
|
|
808
|
+
"arguments": [
|
|
809
|
+
{"name": "pod_name", "required": True, "description": "Pod name"}
|
|
810
|
+
],
|
|
811
|
+
"messages": [
|
|
812
|
+
{"role": "user", "content": "Debug {{pod_name}}"}
|
|
813
|
+
]
|
|
814
|
+
}
|
|
815
|
+
]
|
|
816
|
+
}
|
|
817
|
+
prompts = load_prompts_from_config(config)
|
|
818
|
+
assert len(prompts) == 1
|
|
819
|
+
assert prompts[0].name == "debug-pod"
|
|
820
|
+
assert prompts[0].title == "Debug Pod"
|
|
821
|
+
assert len(prompts[0].arguments) == 1
|
|
822
|
+
assert prompts[0].arguments[0].required is True
|
|
823
|
+
|
|
824
|
+
@pytest.mark.unit
|
|
825
|
+
def test_load_multiple_prompts(self):
|
|
826
|
+
"""Test loading multiple prompts from config."""
|
|
827
|
+
config = {
|
|
828
|
+
"prompts": [
|
|
829
|
+
{"name": "prompt1", "description": "First", "messages": [{"role": "user", "content": "Hello"}]},
|
|
830
|
+
{"name": "prompt2", "description": "Second", "messages": [{"role": "user", "content": "World"}]},
|
|
831
|
+
]
|
|
832
|
+
}
|
|
833
|
+
prompts = load_prompts_from_config(config)
|
|
834
|
+
assert len(prompts) == 2
|
|
835
|
+
|
|
836
|
+
@pytest.mark.unit
|
|
837
|
+
def test_load_empty_config(self):
|
|
838
|
+
"""Test loading from empty config returns empty list."""
|
|
839
|
+
prompts = load_prompts_from_config({})
|
|
840
|
+
assert prompts == []
|
|
841
|
+
|
|
842
|
+
@pytest.mark.unit
|
|
843
|
+
def test_load_config_with_defaults(self):
|
|
844
|
+
"""Test that argument defaults are loaded."""
|
|
845
|
+
config = {
|
|
846
|
+
"prompts": [
|
|
847
|
+
{
|
|
848
|
+
"name": "test",
|
|
849
|
+
"description": "Test",
|
|
850
|
+
"arguments": [
|
|
851
|
+
{"name": "namespace", "required": False, "default": "default"}
|
|
852
|
+
],
|
|
853
|
+
"messages": [{"role": "user", "content": "Test"}]
|
|
854
|
+
}
|
|
855
|
+
]
|
|
856
|
+
}
|
|
857
|
+
prompts = load_prompts_from_config(config)
|
|
858
|
+
assert prompts[0].arguments[0].default == "default"
|
|
859
|
+
|
|
860
|
+
@pytest.mark.unit
|
|
861
|
+
def test_skip_invalid_prompts(self):
|
|
862
|
+
"""Test that invalid prompts are skipped."""
|
|
863
|
+
config = {
|
|
864
|
+
"prompts": [
|
|
865
|
+
{"name": "", "description": "Invalid - no name"}, # Invalid
|
|
866
|
+
{"name": "valid", "description": "Valid prompt", "messages": [{"role": "user", "content": "Hi"}]},
|
|
867
|
+
]
|
|
868
|
+
}
|
|
869
|
+
prompts = load_prompts_from_config(config)
|
|
870
|
+
assert len(prompts) == 1
|
|
871
|
+
assert prompts[0].name == "valid"
|
|
872
|
+
|
|
873
|
+
|
|
874
|
+
class TestLoadPromptsFromTomlFile:
|
|
875
|
+
"""Tests for load_prompts_from_toml_file function."""
|
|
876
|
+
|
|
877
|
+
@pytest.mark.unit
|
|
878
|
+
def test_load_from_valid_toml(self):
|
|
879
|
+
"""Test loading prompts from a valid TOML file."""
|
|
880
|
+
toml_content = """
|
|
881
|
+
[[prompts]]
|
|
882
|
+
name = "test-prompt"
|
|
883
|
+
title = "Test Prompt"
|
|
884
|
+
description = "A test prompt for testing"
|
|
885
|
+
|
|
886
|
+
[[prompts.arguments]]
|
|
887
|
+
name = "target"
|
|
888
|
+
description = "Target resource"
|
|
889
|
+
required = true
|
|
890
|
+
|
|
891
|
+
[[prompts.messages]]
|
|
892
|
+
role = "user"
|
|
893
|
+
content = "Check {{target}}"
|
|
894
|
+
"""
|
|
895
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.toml', delete=False) as f:
|
|
896
|
+
f.write(toml_content)
|
|
897
|
+
f.flush()
|
|
898
|
+
try:
|
|
899
|
+
prompts = load_prompts_from_toml_file(f.name)
|
|
900
|
+
assert len(prompts) == 1
|
|
901
|
+
assert prompts[0].name == "test-prompt"
|
|
902
|
+
assert len(prompts[0].arguments) == 1
|
|
903
|
+
finally:
|
|
904
|
+
os.unlink(f.name)
|
|
905
|
+
|
|
906
|
+
@pytest.mark.unit
|
|
907
|
+
def test_load_from_nonexistent_file(self):
|
|
908
|
+
"""Test loading from nonexistent file returns empty list."""
|
|
909
|
+
prompts = load_prompts_from_toml_file("/nonexistent/path/prompts.toml")
|
|
910
|
+
assert prompts == []
|
|
911
|
+
|
|
912
|
+
|
|
913
|
+
class TestValidatePromptArgs:
|
|
914
|
+
"""Tests for validate_prompt_args function."""
|
|
915
|
+
|
|
916
|
+
@pytest.mark.unit
|
|
917
|
+
def test_valid_required_args(self):
|
|
918
|
+
"""Test validation passes with all required args."""
|
|
919
|
+
prompt = CustomPrompt(
|
|
920
|
+
name="test",
|
|
921
|
+
description="Test",
|
|
922
|
+
arguments=[
|
|
923
|
+
PromptArgument(name="required_arg", required=True),
|
|
924
|
+
]
|
|
925
|
+
)
|
|
926
|
+
errors = validate_prompt_args(prompt, {"required_arg": "value"})
|
|
927
|
+
assert errors == []
|
|
928
|
+
|
|
929
|
+
@pytest.mark.unit
|
|
930
|
+
def test_missing_required_arg(self):
|
|
931
|
+
"""Test validation fails with missing required arg."""
|
|
932
|
+
prompt = CustomPrompt(
|
|
933
|
+
name="test",
|
|
934
|
+
description="Test",
|
|
935
|
+
arguments=[
|
|
936
|
+
PromptArgument(name="required_arg", required=True),
|
|
937
|
+
]
|
|
938
|
+
)
|
|
939
|
+
errors = validate_prompt_args(prompt, {})
|
|
940
|
+
assert len(errors) == 1
|
|
941
|
+
assert "required_arg" in errors[0]
|
|
942
|
+
|
|
943
|
+
@pytest.mark.unit
|
|
944
|
+
def test_empty_required_arg(self):
|
|
945
|
+
"""Test validation fails with empty required arg."""
|
|
946
|
+
prompt = CustomPrompt(
|
|
947
|
+
name="test",
|
|
948
|
+
description="Test",
|
|
949
|
+
arguments=[
|
|
950
|
+
PromptArgument(name="required_arg", required=True),
|
|
951
|
+
]
|
|
952
|
+
)
|
|
953
|
+
errors = validate_prompt_args(prompt, {"required_arg": ""})
|
|
954
|
+
assert len(errors) == 1
|
|
955
|
+
|
|
956
|
+
@pytest.mark.unit
|
|
957
|
+
def test_optional_args_not_required(self):
|
|
958
|
+
"""Test validation passes without optional args."""
|
|
959
|
+
prompt = CustomPrompt(
|
|
960
|
+
name="test",
|
|
961
|
+
description="Test",
|
|
962
|
+
arguments=[
|
|
963
|
+
PromptArgument(name="optional_arg", required=False),
|
|
964
|
+
]
|
|
965
|
+
)
|
|
966
|
+
errors = validate_prompt_args(prompt, {})
|
|
967
|
+
assert errors == []
|
|
968
|
+
|
|
969
|
+
|
|
970
|
+
class TestApplyDefaults:
|
|
971
|
+
"""Tests for apply_defaults function."""
|
|
972
|
+
|
|
973
|
+
@pytest.mark.unit
|
|
974
|
+
def test_apply_missing_defaults(self):
|
|
975
|
+
"""Test that defaults are applied for missing args."""
|
|
976
|
+
prompt = CustomPrompt(
|
|
977
|
+
name="test",
|
|
978
|
+
description="Test",
|
|
979
|
+
arguments=[
|
|
980
|
+
PromptArgument(name="namespace", default="default"),
|
|
981
|
+
PromptArgument(name="timeout", default="30"),
|
|
982
|
+
]
|
|
983
|
+
)
|
|
984
|
+
result = apply_defaults(prompt, {})
|
|
985
|
+
assert result["namespace"] == "default"
|
|
986
|
+
assert result["timeout"] == "30"
|
|
987
|
+
|
|
988
|
+
@pytest.mark.unit
|
|
989
|
+
def test_preserve_provided_values(self):
|
|
990
|
+
"""Test that provided values are preserved."""
|
|
991
|
+
prompt = CustomPrompt(
|
|
992
|
+
name="test",
|
|
993
|
+
description="Test",
|
|
994
|
+
arguments=[
|
|
995
|
+
PromptArgument(name="namespace", default="default"),
|
|
996
|
+
]
|
|
997
|
+
)
|
|
998
|
+
result = apply_defaults(prompt, {"namespace": "production"})
|
|
999
|
+
assert result["namespace"] == "production"
|
|
1000
|
+
|
|
1001
|
+
@pytest.mark.unit
|
|
1002
|
+
def test_no_default_for_empty_string(self):
|
|
1003
|
+
"""Test that empty string default is not applied."""
|
|
1004
|
+
prompt = CustomPrompt(
|
|
1005
|
+
name="test",
|
|
1006
|
+
description="Test",
|
|
1007
|
+
arguments=[
|
|
1008
|
+
PromptArgument(name="arg", default=""),
|
|
1009
|
+
]
|
|
1010
|
+
)
|
|
1011
|
+
result = apply_defaults(prompt, {})
|
|
1012
|
+
assert "arg" not in result
|
|
1013
|
+
|
|
1014
|
+
|
|
1015
|
+
class TestGetPromptSchema:
|
|
1016
|
+
"""Tests for get_prompt_schema function."""
|
|
1017
|
+
|
|
1018
|
+
@pytest.mark.unit
|
|
1019
|
+
def test_generate_schema(self):
|
|
1020
|
+
"""Test generating JSON Schema for prompt."""
|
|
1021
|
+
prompt = CustomPrompt(
|
|
1022
|
+
name="test",
|
|
1023
|
+
description="Test",
|
|
1024
|
+
arguments=[
|
|
1025
|
+
PromptArgument(name="pod_name", description="Pod name", required=True),
|
|
1026
|
+
PromptArgument(name="namespace", description="Namespace", required=False, default="default"),
|
|
1027
|
+
]
|
|
1028
|
+
)
|
|
1029
|
+
schema = get_prompt_schema(prompt)
|
|
1030
|
+
|
|
1031
|
+
assert schema["type"] == "object"
|
|
1032
|
+
assert "pod_name" in schema["properties"]
|
|
1033
|
+
assert "namespace" in schema["properties"]
|
|
1034
|
+
assert schema["properties"]["namespace"]["default"] == "default"
|
|
1035
|
+
assert "pod_name" in schema["required"]
|
|
1036
|
+
assert "namespace" not in schema["required"]
|
|
1037
|
+
|
|
1038
|
+
|
|
1039
|
+
# =============================================================================
|
|
1040
|
+
# Built-in Prompts Tests
|
|
1041
|
+
# =============================================================================
|
|
1042
|
+
|
|
1043
|
+
|
|
1044
|
+
class TestBuiltinPrompts:
|
|
1045
|
+
"""Tests for built-in prompts."""
|
|
1046
|
+
|
|
1047
|
+
@pytest.mark.unit
|
|
1048
|
+
def test_all_builtin_prompts_exist(self):
|
|
1049
|
+
"""Test that all expected built-in prompts exist."""
|
|
1050
|
+
assert len(BUILTIN_PROMPTS) == 6
|
|
1051
|
+
names = [p.name for p in BUILTIN_PROMPTS]
|
|
1052
|
+
assert "cluster-health-check" in names
|
|
1053
|
+
assert "debug-workload" in names
|
|
1054
|
+
assert "resource-usage" in names
|
|
1055
|
+
assert "security-posture" in names
|
|
1056
|
+
assert "deployment-checklist" in names
|
|
1057
|
+
assert "incident-response" in names
|
|
1058
|
+
|
|
1059
|
+
@pytest.mark.unit
|
|
1060
|
+
def test_get_builtin_prompts_returns_copy(self):
|
|
1061
|
+
"""Test that get_builtin_prompts returns a copy."""
|
|
1062
|
+
prompts1 = get_builtin_prompts()
|
|
1063
|
+
prompts2 = get_builtin_prompts()
|
|
1064
|
+
assert prompts1 is not prompts2
|
|
1065
|
+
assert len(prompts1) == len(prompts2)
|
|
1066
|
+
|
|
1067
|
+
@pytest.mark.unit
|
|
1068
|
+
def test_get_builtin_prompt_by_name(self):
|
|
1069
|
+
"""Test retrieving a built-in prompt by name."""
|
|
1070
|
+
prompt = get_builtin_prompt_by_name("cluster-health-check")
|
|
1071
|
+
assert prompt is not None
|
|
1072
|
+
assert prompt.name == "cluster-health-check"
|
|
1073
|
+
assert prompt.title == "Cluster Health Check"
|
|
1074
|
+
|
|
1075
|
+
@pytest.mark.unit
|
|
1076
|
+
def test_get_builtin_prompt_not_found(self):
|
|
1077
|
+
"""Test retrieving a non-existent built-in prompt."""
|
|
1078
|
+
prompt = get_builtin_prompt_by_name("nonexistent")
|
|
1079
|
+
assert prompt is None
|
|
1080
|
+
|
|
1081
|
+
|
|
1082
|
+
class TestClusterHealthCheckPrompt:
|
|
1083
|
+
"""Tests for CLUSTER_HEALTH_CHECK built-in prompt."""
|
|
1084
|
+
|
|
1085
|
+
@pytest.mark.unit
|
|
1086
|
+
def test_prompt_structure(self):
|
|
1087
|
+
"""Test CLUSTER_HEALTH_CHECK prompt structure."""
|
|
1088
|
+
assert CLUSTER_HEALTH_CHECK.name == "cluster-health-check"
|
|
1089
|
+
assert CLUSTER_HEALTH_CHECK.title == "Cluster Health Check"
|
|
1090
|
+
assert len(CLUSTER_HEALTH_CHECK.arguments) == 3
|
|
1091
|
+
assert len(CLUSTER_HEALTH_CHECK.messages) == 1
|
|
1092
|
+
|
|
1093
|
+
@pytest.mark.unit
|
|
1094
|
+
def test_prompt_arguments(self):
|
|
1095
|
+
"""Test CLUSTER_HEALTH_CHECK prompt arguments."""
|
|
1096
|
+
arg_names = [a.name for a in CLUSTER_HEALTH_CHECK.arguments]
|
|
1097
|
+
assert "namespace" in arg_names
|
|
1098
|
+
assert "check_events" in arg_names
|
|
1099
|
+
assert "check_metrics" in arg_names
|
|
1100
|
+
|
|
1101
|
+
@pytest.mark.unit
|
|
1102
|
+
def test_render_with_namespace(self):
|
|
1103
|
+
"""Test rendering with namespace specified."""
|
|
1104
|
+
result = render_prompt(CLUSTER_HEALTH_CHECK, {"namespace": "production"})
|
|
1105
|
+
assert "in namespace production" in result[0].content
|
|
1106
|
+
|
|
1107
|
+
@pytest.mark.unit
|
|
1108
|
+
def test_render_without_namespace(self):
|
|
1109
|
+
"""Test rendering without namespace shows cluster-wide."""
|
|
1110
|
+
result = render_prompt(CLUSTER_HEALTH_CHECK, {})
|
|
1111
|
+
# Should not have "in namespace" since namespace is empty
|
|
1112
|
+
assert result[0].content.count("in namespace ") == 0 or "namespace" not in result[0].content.split("in namespace ")[1].split()[0] if "in namespace " in result[0].content else True
|
|
1113
|
+
|
|
1114
|
+
|
|
1115
|
+
class TestDebugWorkloadPrompt:
|
|
1116
|
+
"""Tests for DEBUG_WORKLOAD built-in prompt."""
|
|
1117
|
+
|
|
1118
|
+
@pytest.mark.unit
|
|
1119
|
+
def test_prompt_structure(self):
|
|
1120
|
+
"""Test DEBUG_WORKLOAD prompt structure."""
|
|
1121
|
+
assert DEBUG_WORKLOAD.name == "debug-workload"
|
|
1122
|
+
assert DEBUG_WORKLOAD.title == "Debug Workload Issues"
|
|
1123
|
+
assert len(DEBUG_WORKLOAD.arguments) == 4
|
|
1124
|
+
|
|
1125
|
+
@pytest.mark.unit
|
|
1126
|
+
def test_required_arguments(self):
|
|
1127
|
+
"""Test DEBUG_WORKLOAD has required workload_name argument."""
|
|
1128
|
+
workload_arg = next(a for a in DEBUG_WORKLOAD.arguments if a.name == "workload_name")
|
|
1129
|
+
assert workload_arg.required is True
|
|
1130
|
+
|
|
1131
|
+
@pytest.mark.unit
|
|
1132
|
+
def test_render_with_all_args(self):
|
|
1133
|
+
"""Test rendering with all arguments."""
|
|
1134
|
+
result = render_prompt(DEBUG_WORKLOAD, {
|
|
1135
|
+
"workload_name": "nginx",
|
|
1136
|
+
"namespace": "production",
|
|
1137
|
+
"workload_type": "deployment",
|
|
1138
|
+
"include_related": "true"
|
|
1139
|
+
})
|
|
1140
|
+
assert "nginx" in result[0].content
|
|
1141
|
+
assert "production" in result[0].content
|
|
1142
|
+
assert "deployment" in result[0].content
|
|
1143
|
+
|
|
1144
|
+
|
|
1145
|
+
class TestResourceUsagePrompt:
|
|
1146
|
+
"""Tests for RESOURCE_USAGE built-in prompt."""
|
|
1147
|
+
|
|
1148
|
+
@pytest.mark.unit
|
|
1149
|
+
def test_prompt_structure(self):
|
|
1150
|
+
"""Test RESOURCE_USAGE prompt structure."""
|
|
1151
|
+
assert RESOURCE_USAGE.name == "resource-usage"
|
|
1152
|
+
assert len(RESOURCE_USAGE.arguments) == 4
|
|
1153
|
+
|
|
1154
|
+
@pytest.mark.unit
|
|
1155
|
+
def test_default_thresholds(self):
|
|
1156
|
+
"""Test default threshold values."""
|
|
1157
|
+
cpu_arg = next(a for a in RESOURCE_USAGE.arguments if a.name == "threshold_cpu")
|
|
1158
|
+
mem_arg = next(a for a in RESOURCE_USAGE.arguments if a.name == "threshold_memory")
|
|
1159
|
+
assert cpu_arg.default == "80"
|
|
1160
|
+
assert mem_arg.default == "80"
|
|
1161
|
+
|
|
1162
|
+
|
|
1163
|
+
class TestSecurityPosturePrompt:
|
|
1164
|
+
"""Tests for SECURITY_POSTURE built-in prompt."""
|
|
1165
|
+
|
|
1166
|
+
@pytest.mark.unit
|
|
1167
|
+
def test_prompt_structure(self):
|
|
1168
|
+
"""Test SECURITY_POSTURE prompt structure."""
|
|
1169
|
+
assert SECURITY_POSTURE.name == "security-posture"
|
|
1170
|
+
assert len(SECURITY_POSTURE.arguments) == 4
|
|
1171
|
+
|
|
1172
|
+
@pytest.mark.unit
|
|
1173
|
+
def test_conditional_sections(self):
|
|
1174
|
+
"""Test conditional sections for RBAC, network, secrets checks."""
|
|
1175
|
+
result = render_prompt(SECURITY_POSTURE, {
|
|
1176
|
+
"check_rbac": "true",
|
|
1177
|
+
"check_network": "false",
|
|
1178
|
+
"check_secrets": "true"
|
|
1179
|
+
})
|
|
1180
|
+
assert "RBAC Analysis" in result[0].content
|
|
1181
|
+
assert "Network Security" not in result[0].content
|
|
1182
|
+
assert "Secrets Management" in result[0].content
|
|
1183
|
+
|
|
1184
|
+
|
|
1185
|
+
class TestDeploymentChecklistPrompt:
|
|
1186
|
+
"""Tests for DEPLOYMENT_CHECKLIST built-in prompt."""
|
|
1187
|
+
|
|
1188
|
+
@pytest.mark.unit
|
|
1189
|
+
def test_prompt_structure(self):
|
|
1190
|
+
"""Test DEPLOYMENT_CHECKLIST prompt structure."""
|
|
1191
|
+
assert DEPLOYMENT_CHECKLIST.name == "deployment-checklist"
|
|
1192
|
+
assert len(DEPLOYMENT_CHECKLIST.arguments) == 4
|
|
1193
|
+
|
|
1194
|
+
@pytest.mark.unit
|
|
1195
|
+
def test_required_arguments(self):
|
|
1196
|
+
"""Test required arguments."""
|
|
1197
|
+
required_args = [a.name for a in DEPLOYMENT_CHECKLIST.arguments if a.required]
|
|
1198
|
+
assert "app_name" in required_args
|
|
1199
|
+
assert "namespace" in required_args
|
|
1200
|
+
assert "image" in required_args
|
|
1201
|
+
|
|
1202
|
+
@pytest.mark.unit
|
|
1203
|
+
def test_render_with_args(self):
|
|
1204
|
+
"""Test rendering with deployment args."""
|
|
1205
|
+
result = render_prompt(DEPLOYMENT_CHECKLIST, {
|
|
1206
|
+
"app_name": "myapp",
|
|
1207
|
+
"namespace": "production",
|
|
1208
|
+
"image": "myapp:v1.2.3",
|
|
1209
|
+
"replicas": "3"
|
|
1210
|
+
})
|
|
1211
|
+
assert "myapp" in result[0].content
|
|
1212
|
+
assert "production" in result[0].content
|
|
1213
|
+
assert "myapp:v1.2.3" in result[0].content
|
|
1214
|
+
assert "3" in result[0].content
|
|
1215
|
+
|
|
1216
|
+
|
|
1217
|
+
class TestIncidentResponsePrompt:
|
|
1218
|
+
"""Tests for INCIDENT_RESPONSE built-in prompt."""
|
|
1219
|
+
|
|
1220
|
+
@pytest.mark.unit
|
|
1221
|
+
def test_prompt_structure(self):
|
|
1222
|
+
"""Test INCIDENT_RESPONSE prompt structure."""
|
|
1223
|
+
assert INCIDENT_RESPONSE.name == "incident-response"
|
|
1224
|
+
assert len(INCIDENT_RESPONSE.arguments) == 4
|
|
1225
|
+
|
|
1226
|
+
@pytest.mark.unit
|
|
1227
|
+
def test_required_arguments(self):
|
|
1228
|
+
"""Test required arguments."""
|
|
1229
|
+
required_args = [a.name for a in INCIDENT_RESPONSE.arguments if a.required]
|
|
1230
|
+
assert "incident_type" in required_args
|
|
1231
|
+
assert "affected_service" in required_args
|
|
1232
|
+
assert "namespace" in required_args
|
|
1233
|
+
|
|
1234
|
+
@pytest.mark.unit
|
|
1235
|
+
def test_default_severity(self):
|
|
1236
|
+
"""Test default severity."""
|
|
1237
|
+
severity_arg = next(a for a in INCIDENT_RESPONSE.arguments if a.name == "severity")
|
|
1238
|
+
assert severity_arg.default == "high"
|
|
1239
|
+
|
|
1240
|
+
@pytest.mark.unit
|
|
1241
|
+
def test_render_incident(self):
|
|
1242
|
+
"""Test rendering incident response."""
|
|
1243
|
+
result = render_prompt(INCIDENT_RESPONSE, {
|
|
1244
|
+
"incident_type": "pod-crash",
|
|
1245
|
+
"affected_service": "api-server",
|
|
1246
|
+
"namespace": "production",
|
|
1247
|
+
"severity": "critical"
|
|
1248
|
+
})
|
|
1249
|
+
assert "pod-crash" in result[0].content
|
|
1250
|
+
assert "api-server" in result[0].content
|
|
1251
|
+
assert "production" in result[0].content
|
|
1252
|
+
assert "critical" in result[0].content
|