refactor: 문서 개선 중
This commit is contained in:
@@ -0,0 +1,565 @@
|
||||
"""명령어 교육성 분석기 회귀 시험.
|
||||
|
||||
정확한 명령을 금지하는 검사가 아니라, 사람이 따라 하기 어려운 압축 표현을
|
||||
후속 planner/editor/reviewer가 볼 수 있게 결정론적으로 표시하는지 본다.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
sys.path.insert(0, os.path.join(ROOT, "scripts"))
|
||||
|
||||
from command_pedagogy import ( # noqa: E402
|
||||
analyze_commands,
|
||||
apply_command_patch_set,
|
||||
validate_command_patch_set,
|
||||
validate_command_plan,
|
||||
)
|
||||
|
||||
|
||||
class CommandPedagogyAnalyzerTest(unittest.TestCase):
|
||||
def codes(self, text: str) -> set[str]:
|
||||
result = analyze_commands("record", text)
|
||||
return {finding["code"] for finding in result["findings"]}
|
||||
|
||||
def test_sed_in_place_and_command_substitution_are_signals(self):
|
||||
text = '''```bash
|
||||
sed -i \\
|
||||
-e "s|__LAB_HOST_KEY__|$(cat ~/.ssh/id_ed25519.pub)|" \\
|
||||
lab.yaml
|
||||
```
|
||||
'''
|
||||
self.assertEqual(
|
||||
{"in-place-text-rewrite", "command-substitution"},
|
||||
self.codes(text),
|
||||
)
|
||||
|
||||
def test_sed_print_mode_is_not_in_place_rewrite(self):
|
||||
text = r'''```bash
|
||||
sed -n '/^COPY public.databasechangelog /,/^\\\.$/p' dump.sql | wc -l
|
||||
```
|
||||
'''
|
||||
self.assertNotIn("in-place-text-rewrite", self.codes(text))
|
||||
|
||||
def test_remote_compound_flow_exposes_ip_and_cleanup_chain(self):
|
||||
text = '''```bash
|
||||
ssh donghyeon@192.168.122.11 'cloud-init schema -c ~/kc-lab-2.yaml && rm ~/kc-lab-2.yaml'
|
||||
```
|
||||
'''
|
||||
self.assertEqual(
|
||||
{
|
||||
"raw-ssh-ip",
|
||||
"compound-remote-shell",
|
||||
"cleanup-chained-with-verification",
|
||||
"execution-context-implicit",
|
||||
},
|
||||
self.codes(text),
|
||||
)
|
||||
result = analyze_commands("record", text)
|
||||
severity = {finding["code"]: finding["severity"] for finding in result["findings"]}
|
||||
self.assertEqual("minor", severity["compound-remote-shell"])
|
||||
self.assertEqual("major", severity["cleanup-chained-with-verification"])
|
||||
|
||||
def test_reference_mode_keeps_historical_cleanup_as_review_signal(self):
|
||||
text = '''<!-- command-mode: reference -->
|
||||
```bash
|
||||
ssh host 'cloud-init schema -c ~/lab.yaml; rm -f ~/lab.yaml'
|
||||
```
|
||||
'''
|
||||
result = analyze_commands("record", text, mode="operator")
|
||||
severity = {
|
||||
finding["code"]: finding["severity"]
|
||||
for finding in result["findings"]
|
||||
}
|
||||
self.assertEqual(
|
||||
"minor",
|
||||
severity["cleanup-chained-with-verification"],
|
||||
)
|
||||
self.assertEqual("reference", result["blocks"][0]["mode"])
|
||||
|
||||
def test_label_mode_marker_can_mark_reference_without_html_marker(self):
|
||||
text = '''```bash label="[lab host] [reference] historical"
|
||||
ssh host 'cloud-init schema -c ~/lab.yaml; rm -f ~/lab.yaml'
|
||||
```
|
||||
'''
|
||||
result = analyze_commands("record", text, mode="operator")
|
||||
self.assertEqual("reference", result["blocks"][0]["mode"])
|
||||
self.assertTrue(result["blocks"][0]["mode_explicit"])
|
||||
severity = {
|
||||
finding["code"]: finding["severity"]
|
||||
for finding in result["findings"]
|
||||
}
|
||||
self.assertEqual(
|
||||
"minor",
|
||||
severity["cleanup-chained-with-verification"],
|
||||
)
|
||||
|
||||
def test_printf_generated_file_and_substitution_are_signals(self):
|
||||
text = '''```bash
|
||||
printf 'instance-id: kc-lab-1-%s\\nlocal-hostname: kc-lab-1\\n' "$(date +%s)" > meta-kc-lab-1
|
||||
```
|
||||
'''
|
||||
self.assertEqual(
|
||||
{"opaque-file-generation", "command-substitution"},
|
||||
self.codes(text),
|
||||
)
|
||||
|
||||
def test_hidden_stderr_and_compressed_pipeline_are_signals(self):
|
||||
text = '''```bash
|
||||
openssl s_client -connect example.com:443 2>/dev/null | grep subject | head -1
|
||||
```
|
||||
'''
|
||||
self.assertEqual(
|
||||
{"hidden-stderr", "compressed-pipeline"},
|
||||
self.codes(text),
|
||||
)
|
||||
|
||||
def test_simple_operator_commands_are_not_globally_banned(self):
|
||||
text = '''```bash
|
||||
kubectl get pods
|
||||
kubectl describe pod api-0
|
||||
```
|
||||
'''
|
||||
result = analyze_commands("record", text)
|
||||
self.assertEqual("PASS", result["result"])
|
||||
self.assertFalse(result["requires_editor"])
|
||||
self.assertEqual(1, len(result["blocks"]))
|
||||
self.assertEqual([], result["findings"])
|
||||
|
||||
def test_non_shell_fences_are_out_of_scope(self):
|
||||
result = analyze_commands("record", '''```yaml
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
```
|
||||
''')
|
||||
self.assertEqual([], result["blocks"])
|
||||
self.assertEqual([], result["findings"])
|
||||
|
||||
def test_block_metadata_is_stable_and_bounded(self):
|
||||
text = '''앞 문장
|
||||
|
||||
```sh
|
||||
echo hello
|
||||
```
|
||||
|
||||
뒤 문장
|
||||
'''
|
||||
result = analyze_commands("record", text)
|
||||
block = result["blocks"][0]
|
||||
self.assertEqual("command-001", block["id"])
|
||||
self.assertEqual("sh", block["language"])
|
||||
self.assertEqual(text[block["start"]:block["end"]], block["source"])
|
||||
self.assertEqual(64, len(block["source_sha256"]))
|
||||
self.assertEqual(64, len(result["source_sha256"]))
|
||||
|
||||
def test_labeled_shell_fence_uses_first_info_token_as_language(self):
|
||||
text = '''```bash label="[lab host] 파드 상태를 본다"
|
||||
kubectl get pods
|
||||
```
|
||||
'''
|
||||
result = analyze_commands("record", text)
|
||||
self.assertEqual(1, len(result["blocks"]))
|
||||
block = result["blocks"][0]
|
||||
self.assertEqual("bash", block["language"])
|
||||
self.assertEqual('label="[lab host] 파드 상태를 본다"', block["info_string"])
|
||||
self.assertEqual("[lab host] 파드 상태를 본다", block["label"])
|
||||
|
||||
def test_labeled_sh_fence_is_analyzed(self):
|
||||
text = '''```sh label="[탐침 파드] 연결을 확인한다"
|
||||
curl -fsS http://keycloak:8080/health/ready
|
||||
```
|
||||
'''
|
||||
result = analyze_commands("record", text)
|
||||
self.assertEqual(1, len(result["blocks"]))
|
||||
self.assertEqual("sh", result["blocks"][0]["language"])
|
||||
|
||||
def test_language_must_be_the_complete_first_info_token(self):
|
||||
result = analyze_commands("record", '''```bashish label="[lab host]"
|
||||
echo nope
|
||||
```
|
||||
''')
|
||||
self.assertEqual([], result["blocks"])
|
||||
|
||||
def test_non_context_label_does_not_hide_remote_context_signal(self):
|
||||
text = '''```bash label="[결과 확인] 원격 상태를 본다"
|
||||
ssh test-server
|
||||
```
|
||||
'''
|
||||
result = analyze_commands("record", text)
|
||||
codes = {finding["code"] for finding in result["findings"]}
|
||||
self.assertIn("execution-context-implicit", codes)
|
||||
|
||||
def test_execution_context_in_label_suppresses_context_signal(self):
|
||||
text = '''```bash label="[워크스테이션] test-server에 접속한다"
|
||||
ssh test-server
|
||||
```
|
||||
'''
|
||||
result = analyze_commands("record", text)
|
||||
codes = {finding["code"] for finding in result["findings"]}
|
||||
self.assertNotIn("execution-context-implicit", codes)
|
||||
|
||||
|
||||
def test_inline_command_mode_overrides_document_default(self):
|
||||
text = '''일반 참고 문서다.
|
||||
|
||||
<!-- command-mode: operator -->
|
||||
```bash
|
||||
echo "$(date +%s)"
|
||||
```
|
||||
'''
|
||||
result = analyze_commands("record", text, mode="reference")
|
||||
self.assertEqual("operator", result["blocks"][0]["mode"])
|
||||
self.assertIn(
|
||||
"command-substitution",
|
||||
{finding["code"] for finding in result["findings"]},
|
||||
)
|
||||
|
||||
def test_inline_command_mode_applies_only_to_the_next_block(self):
|
||||
text = '''<!-- command-mode: operator -->
|
||||
```bash
|
||||
echo "$(date +%s)"
|
||||
```
|
||||
|
||||
```bash
|
||||
echo "$(date +%s)"
|
||||
```
|
||||
'''
|
||||
result = analyze_commands("record", text, mode="reference")
|
||||
self.assertTrue(result["blocks"][0]["mode_explicit"])
|
||||
self.assertEqual("operator", result["blocks"][0]["mode"])
|
||||
self.assertFalse(result["blocks"][1]["mode_explicit"])
|
||||
self.assertEqual("reference", result["blocks"][1]["mode"])
|
||||
substitution_blocks = {
|
||||
finding["block_id"]
|
||||
for finding in result["findings"]
|
||||
if finding["code"] == "command-substitution"
|
||||
}
|
||||
self.assertEqual({"command-001"}, substitution_blocks)
|
||||
|
||||
def test_text_fence_with_cli_flow_is_reported_as_extension(self):
|
||||
text = '''```text
|
||||
docker build . → docker save app | gzip → scp app.tar.gz test-server:/tmp/
|
||||
→ kubectl set image deployment/app app=local/app:test
|
||||
```
|
||||
'''
|
||||
result = analyze_commands("record", text, mode="reference")
|
||||
self.assertEqual([], result["blocks"])
|
||||
self.assertEqual([], result["findings"])
|
||||
self.assertEqual(
|
||||
1,
|
||||
len(result["extensions"]["command_like_text_blocks"]),
|
||||
)
|
||||
|
||||
def test_explicit_reference_text_flow_is_not_reported_as_misfenced_command(self):
|
||||
text = '''<!-- command-mode: reference -->
|
||||
```text
|
||||
docker build . → docker save app | gzip → scp app.tar.gz test-server:/tmp/
|
||||
→ kubectl set image deployment/app app=local/app:test
|
||||
```
|
||||
'''
|
||||
result = analyze_commands("record", text, mode="reference")
|
||||
self.assertEqual([], result["extensions"]["command_like_text_blocks"])
|
||||
|
||||
def test_cwd_change_followed_by_new_command_group_is_a_signal(self):
|
||||
text = '''```bash
|
||||
cd src
|
||||
./gradlew build
|
||||
|
||||
# 다른 도구
|
||||
python3 scripts/check.py
|
||||
```
|
||||
'''
|
||||
result = analyze_commands("record", text, mode="operator")
|
||||
self.assertIn(
|
||||
"cwd-transition-crosses-command-group",
|
||||
{finding["code"] for finding in result["findings"]},
|
||||
)
|
||||
|
||||
|
||||
class CommandPatchBoundaryTest(unittest.TestCase):
|
||||
def test_patch_can_replace_only_the_named_command_span(self):
|
||||
text = '''앞 문장은 그대로다.\n\n```bash\nprintf 'x=%s\\n' "$(date +%s)" > x.conf\n```\n\n뒤 문장도 그대로다.\n'''
|
||||
analysis = analyze_commands("record", text)
|
||||
block = analysis["blocks"][0]
|
||||
replacement = '''```bash\nnano x.conf\n```\n```text\nx=20260917\n```'''
|
||||
repaired = apply_command_patch_set(
|
||||
text=text,
|
||||
analysis=analysis,
|
||||
patch_set={
|
||||
"schema_version": "1.0",
|
||||
"source_sha256": analysis["source_sha256"],
|
||||
"patches": [{
|
||||
"block_id": block["id"],
|
||||
"expected_sha256": block["source_sha256"],
|
||||
"replacement_markdown": replacement,
|
||||
}],
|
||||
},
|
||||
)
|
||||
self.assertTrue(repaired.startswith("앞 문장은 그대로다.\n\n"))
|
||||
self.assertTrue(repaired.endswith("\n\n뒤 문장도 그대로다.\n"))
|
||||
self.assertIn(replacement, repaired)
|
||||
self.assertNotIn("printf", repaired)
|
||||
|
||||
def test_stale_block_hash_is_rejected(self):
|
||||
text = '''```bash\nsed -i 's/a/b/' x.conf\n```\n'''
|
||||
analysis = analyze_commands("record", text)
|
||||
with self.assertRaisesRegex(ValueError, "expected block hash"):
|
||||
apply_command_patch_set(
|
||||
text=text,
|
||||
analysis=analysis,
|
||||
patch_set={
|
||||
"schema_version": "1.0",
|
||||
"source_sha256": analysis["source_sha256"],
|
||||
"patches": [{
|
||||
"block_id": "command-001",
|
||||
"expected_sha256": "0" * 64,
|
||||
"replacement_markdown": "```bash\\nnano x.conf\\n```",
|
||||
}],
|
||||
},
|
||||
)
|
||||
|
||||
def test_patch_cannot_reference_an_unknown_block(self):
|
||||
text = '''```bash\necho hello\n```\n'''
|
||||
analysis = analyze_commands("record", text)
|
||||
with self.assertRaisesRegex(ValueError, "unknown block"):
|
||||
apply_command_patch_set(
|
||||
text=text,
|
||||
analysis=analysis,
|
||||
patch_set={
|
||||
"schema_version": "1.0",
|
||||
"source_sha256": analysis["source_sha256"],
|
||||
"patches": [{
|
||||
"block_id": "command-999",
|
||||
"expected_sha256": "0" * 64,
|
||||
"replacement_markdown": "```bash\\necho world\\n```",
|
||||
}],
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class CommandPatchCliTest(unittest.TestCase):
|
||||
def test_cli_applies_a_frozen_patch_set(self):
|
||||
script = os.path.join(ROOT, "scripts", "apply-command-pedagogy-patch.py")
|
||||
text = "앞\n\n```bash\nprintf 'x\\n' > x.conf\n```\n\n뒤\n"
|
||||
analysis = analyze_commands("record", text)
|
||||
block = analysis["blocks"][0]
|
||||
patch_set = {
|
||||
"schema_version": "1.0",
|
||||
"source_sha256": analysis["source_sha256"],
|
||||
"patches": [{
|
||||
"block_id": block["id"],
|
||||
"expected_sha256": block["source_sha256"],
|
||||
"replacement_markdown": "```bash\nnano x.conf\n```",
|
||||
}],
|
||||
}
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
record = os.path.join(d, "record.md")
|
||||
analysis_path = os.path.join(d, "analysis.json")
|
||||
patch_path = os.path.join(d, "patch.json")
|
||||
output = os.path.join(d, "repaired.md")
|
||||
with open(record, "w", encoding="utf-8") as fh:
|
||||
fh.write(text)
|
||||
with open(analysis_path, "w", encoding="utf-8") as fh:
|
||||
json.dump(analysis, fh, ensure_ascii=False)
|
||||
with open(patch_path, "w", encoding="utf-8") as fh:
|
||||
json.dump(patch_set, fh, ensure_ascii=False)
|
||||
p = subprocess.run(
|
||||
[sys.executable, script, record, analysis_path, patch_path, "-o", output],
|
||||
cwd=ROOT,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
self.assertEqual(0, p.returncode, p.stdout + p.stderr)
|
||||
with open(output, encoding="utf-8") as fh:
|
||||
repaired = fh.read()
|
||||
self.assertEqual("앞\n\n```bash\nnano x.conf\n```\n\n뒤\n", repaired)
|
||||
|
||||
|
||||
class CommandPedagogyModeTest(unittest.TestCase):
|
||||
def test_zsh_fence_is_analyzed(self):
|
||||
result = analyze_commands("record", "```zsh\nssh user@192.168.1.10\n```\n")
|
||||
self.assertEqual(1, len(result["blocks"]))
|
||||
self.assertEqual("zsh", result["blocks"][0]["language"])
|
||||
|
||||
def test_nested_command_substitution_is_a_distinct_signal(self):
|
||||
result = analyze_commands(
|
||||
"record",
|
||||
"```bash\necho \"$(printf %s \"$(date +%s)\")\"\n```\n",
|
||||
)
|
||||
codes = {finding["code"] for finding in result["findings"]}
|
||||
self.assertIn("nested-command-substitution", codes)
|
||||
|
||||
def test_automation_mode_does_not_blanket_flag_compact_shell(self):
|
||||
text = '''자동화 스크립트는 입력 파일을 갱신한 뒤 결과를 별도 검증한다.\n\n```bash\nsed -i -e "s|__KEY__|$(cat key.pub)|" config.yaml\ncat config.yaml | grep KEY | head -1\n```\n'''
|
||||
operator = analyze_commands("record", text, mode="operator")
|
||||
automation = analyze_commands("record", text, mode="automation")
|
||||
self.assertTrue(operator["findings"])
|
||||
self.assertEqual("automation", automation["mode"])
|
||||
self.assertEqual([], automation["findings"])
|
||||
|
||||
def test_unknown_mode_is_rejected(self):
|
||||
with self.assertRaisesRegex(ValueError, "unsupported command pedagogy mode"):
|
||||
analyze_commands("record", "```bash\necho ok\n```\n", mode="unknown")
|
||||
|
||||
|
||||
def test_remote_command_without_execution_context_is_a_signal(self):
|
||||
result = analyze_commands("record", "```bash\nssh test-server\n```\n")
|
||||
codes = {finding["code"] for finding in result["findings"]}
|
||||
self.assertIn("execution-context-implicit", codes)
|
||||
|
||||
def test_explicit_execution_context_suppresses_context_signal(self):
|
||||
text = "이 명령은 로컬 워크스테이션에서 실행한다.\n\n```bash\nssh test-server\n```\n"
|
||||
result = analyze_commands("record", text)
|
||||
codes = {finding["code"] for finding in result["findings"]}
|
||||
self.assertNotIn("execution-context-implicit", codes)
|
||||
|
||||
def test_repeated_raw_ssh_ip_is_reported(self):
|
||||
text = '''```bash\nssh user@192.168.122.11\n```\n\n```bash\nscp x user@192.168.122.11:~/x\n```\n'''
|
||||
result = analyze_commands("record", text)
|
||||
codes = [finding["code"] for finding in result["findings"]]
|
||||
self.assertIn("repeated-raw-ssh-host", codes)
|
||||
|
||||
|
||||
class CommandArtifactContractTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.text = "```bash\nssh user@192.168.1.10\n```\n"
|
||||
self.analysis = analyze_commands("section-a", self.text)
|
||||
self.block = self.analysis["blocks"][0]
|
||||
|
||||
def valid_plan(self):
|
||||
return {
|
||||
"schema_version": "1.0",
|
||||
"section_id": "section-a",
|
||||
"source_sha256": self.analysis["source_sha256"],
|
||||
"mode": "operator",
|
||||
"command_groups": [
|
||||
{
|
||||
"id": "connect-host",
|
||||
"block_id": self.block["id"],
|
||||
"source_sha256": self.block["source_sha256"],
|
||||
"goal": "대상 호스트 연결을 확인한다.",
|
||||
"execution_context": {"host": "local", "cwd": "."},
|
||||
"prerequisites": ["SSH key가 준비되어 있다."],
|
||||
"steps": [
|
||||
{
|
||||
"command": "ssh test-server",
|
||||
"reason": "별칭으로 연결한다.",
|
||||
"expected_result": "원격 셸이 열린다.",
|
||||
}
|
||||
],
|
||||
"cleanup": [],
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
def test_valid_command_plan_passes(self):
|
||||
validate_command_plan(self.valid_plan(), analysis=self.analysis)
|
||||
|
||||
def test_command_plan_requires_supported_mode(self):
|
||||
plan = self.valid_plan()
|
||||
plan["mode"] = "clever"
|
||||
with self.assertRaisesRegex(ValueError, "mode"):
|
||||
validate_command_plan(plan, analysis=self.analysis)
|
||||
|
||||
def test_command_plan_rejects_duplicate_group_ids(self):
|
||||
plan = self.valid_plan()
|
||||
plan["command_groups"].append(dict(plan["command_groups"][0]))
|
||||
with self.assertRaisesRegex(ValueError, "duplicate command group id"):
|
||||
validate_command_plan(plan, analysis=self.analysis)
|
||||
|
||||
def test_command_plan_requires_execution_context(self):
|
||||
plan = self.valid_plan()
|
||||
del plan["command_groups"][0]["execution_context"]
|
||||
with self.assertRaisesRegex(ValueError, "execution_context"):
|
||||
validate_command_plan(plan, analysis=self.analysis)
|
||||
|
||||
def test_patch_set_requires_schema_version_and_known_hashes(self):
|
||||
patch = {
|
||||
"schema_version": "1.0",
|
||||
"source_sha256": self.analysis["source_sha256"],
|
||||
"patches": [
|
||||
{
|
||||
"block_id": self.block["id"],
|
||||
"expected_sha256": self.block["source_sha256"],
|
||||
"replacement_markdown": "```bash\nssh test-server\n```",
|
||||
}
|
||||
],
|
||||
}
|
||||
validate_command_patch_set(patch, analysis=self.analysis)
|
||||
bad = dict(patch)
|
||||
bad.pop("schema_version")
|
||||
with self.assertRaisesRegex(ValueError, "schema_version"):
|
||||
validate_command_patch_set(bad, analysis=self.analysis)
|
||||
|
||||
|
||||
class CommandArtifactCliContractTest(unittest.TestCase):
|
||||
def test_analysis_cli_accepts_document_mode(self):
|
||||
script = os.path.join(ROOT, "scripts", "check-command-pedagogy.py")
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
record = os.path.join(d, "record.md")
|
||||
output = os.path.join(d, "analysis.json")
|
||||
with open(record, "w", encoding="utf-8") as fh:
|
||||
fh.write('```bash\nsed -i "s/a/$(cat value)/" config\n```\n')
|
||||
p = subprocess.run(
|
||||
[sys.executable, script, record, "--mode", "automation", "-o", output],
|
||||
cwd=ROOT,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
self.assertEqual(0, p.returncode, p.stdout + p.stderr)
|
||||
with open(output, encoding="utf-8") as fh:
|
||||
result = json.load(fh)
|
||||
self.assertEqual("automation", result["mode"])
|
||||
self.assertEqual([], result["findings"])
|
||||
|
||||
def test_artifact_validator_cli_checks_plan_against_analysis(self):
|
||||
validator = os.path.join(ROOT, "scripts", "validate-command-pedagogy-artifact.py")
|
||||
text = "```bash\nssh user@192.168.1.10\n```\n"
|
||||
analysis = analyze_commands("section-a", text)
|
||||
block = analysis["blocks"][0]
|
||||
plan = {
|
||||
"schema_version": "1.0",
|
||||
"section_id": "section-a",
|
||||
"source_sha256": analysis["source_sha256"],
|
||||
"mode": "operator",
|
||||
"command_groups": [
|
||||
{
|
||||
"id": "connect",
|
||||
"block_id": block["id"],
|
||||
"source_sha256": block["source_sha256"],
|
||||
"goal": "연결한다.",
|
||||
"execution_context": {"host": "local", "cwd": "."},
|
||||
"prerequisites": [],
|
||||
"steps": [{
|
||||
"command": "ssh test-server",
|
||||
"reason": "연결 확인",
|
||||
"expected_result": "원격 셸",
|
||||
}],
|
||||
"cleanup": [],
|
||||
}
|
||||
],
|
||||
}
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
analysis_path = os.path.join(d, "analysis.json")
|
||||
plan_path = os.path.join(d, "plan.json")
|
||||
with open(analysis_path, "w", encoding="utf-8") as fh:
|
||||
json.dump(analysis, fh)
|
||||
with open(plan_path, "w", encoding="utf-8") as fh:
|
||||
json.dump(plan, fh)
|
||||
p = subprocess.run(
|
||||
[sys.executable, validator, "plan", plan_path, "--analysis", analysis_path],
|
||||
cwd=ROOT,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
self.assertEqual(0, p.returncode, p.stdout + p.stderr)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user