Files
mcp-server/tests/unit/test_models.py
T
ClaudeandClaude Fable 5 523e6a9c76 refactor(mcp): rename generate_job_file to write_job_file to match what it does
The MCP tool was named `generate_job_file` from Stage 2 but it does
NOT generate PySpark code — the calling LLM writes the code in its own
context, and this tool only persists it to a file under
SPARK_EXECUTOR_JOBS_DIR so `spark-submit` can see it. The misleading
`generate_` prefix sent agents (and humans) looking for a code
generator that doesn't exist.

This commit folds three related polish changes into one (split later
with rebase -i if you want them as separate history):

  1. The rename itself:
     - `tools/generate.py`  →  `tools/write_job.py`
     - `generate_job_file`  →  `write_job_file`
     - `GenerateJobFileRequest`  →  `WriteJobFileRequest`
     - `/generate_job_file` route  →  `/write_job_file`
     - `operation_id="generate_job_file"`  →  `operation_id="write_job_file"`
     The internal helper `core.job_writer.write_job_file` (which just
     writes bytes to disk with no SQL guard) is imported with an
     `_write_to_disk` alias to avoid the name collision with the
     MCP-exposed function in the same module.
     The description for the tool now explicitly states 'this tool
     does NOT generate PySpark code. The calling LLM is expected to
     have already written the code; this tool only persists it.'

  2. Skill for LLM agents operating the service
     (`docs/superpowers/skills/spark-executor-mcp-operate/SKILL.md`,
     449 lines). Covers the 16 tools, the two-step prepare/confirm
     flow, the dual-ID contract (job_id vs application_id), the
     PendingSubmission state machine, the Connection profile, the
     job-file workflow, the error reference, common pitfalls, and a
     full end-to-end word-count example.

  3. Default `executor_memory` lowered 4G → 2G
     (`_DEFAULTS_TO_CONFIRM` in `server.py`). Mirrors the matching
     change in `test_mcp_routes.py` and the 5 unit tests that
     reference the default. Aligns with the lighter workloads the
     service is sized for in its current container profile.

Also tracked in git for the first time:
  - `docs/superpowers/plans/2026-06-24-spark-executor-mcp.md`
    (the original Stage 1/2/3 design plan, updated to use the new
    tool name throughout).

Test rename:
  - `tests/unit/test_generate_tool.py`  →  `test_write_job_tool.py`
  - the new test file picks up an extra assertion that the SQL guard
    rejects a `DROP TABLE` statement at write time.

243 tests pass (was 242; +1 new SQL-guard assertion). Zero regressions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-29 19:13:29 +08:00

155 lines
4.6 KiB
Python

# coding=utf-8
from datetime import datetime
from spark_executor.models import Connection, Job, JobStatus, PendingSubmission, SubmitResult
def test_job_roundtrip():
job = Job(
job_id="abc123",
application_id="application_17400000001",
script_path="/tmp/jobs/job_001.py",
queue="default",
submit_time=datetime(2026, 6, 24, 10, 0, 0),
connection="prod-yarn",
yarn_rm_url="http://rm:8088",
)
dumped = job.model_dump()
assert dumped["job_id"] == "abc123"
assert dumped["application_id"] == "application_17400000001"
assert dumped["connection"] == "prod-yarn"
assert dumped["yarn_rm_url"] == "http://rm:8088"
def test_job_yarn_rm_url_optional():
job = Job(
job_id="j1",
application_id="application_1",
script_path="/tmp/x.py",
queue="default",
submit_time=datetime(2026, 6, 24),
connection="dev",
)
assert job.yarn_rm_url is None
def test_job_status_default_raw():
s = JobStatus(application_id="application_1", state="RUNNING")
assert s.raw == ""
def test_submit_result_tracking_url_optional():
r = SubmitResult(job_id="j1", application_id="application_1", tracking_url=None)
assert r.tracking_url is None
def test_connection_defaults():
c = Connection(name="prod", master="yarn")
assert c.master == "yarn"
assert c.deploy_mode == "cluster"
assert c.yarn_rm_url is None
assert c.spark_conf == {}
assert c.ssl_verify is None
assert c.ssl_ca_bundle is None
def test_connection_master_defaults_to_yarn():
"""YARN users should not have to write master='yarn' explicitly."""
c = Connection(name="prod")
assert c.master == "yarn"
def test_connection_with_spark_conf():
c = Connection(
name="dev",
master="spark://master:7077",
deploy_mode="client",
spark_conf={"spark.sql.shuffle.partitions": "200"},
)
assert c.spark_conf["spark.sql.shuffle.partitions"] == "200"
def test_connection_accepts_other_masters():
"""Standalone, k8s, and local should pass validation."""
Connection(name="standalone", master="spark://host:7077")
Connection(name="k8s", master="k8s://https://api.example.com:443")
Connection(name="local", master="local[4]")
def test_connection_rejects_bad_master():
"""Common typos like 'yarn-cluster' or 'http://...' should fail loudly."""
import pytest
with pytest.raises(ValueError, match="must be 'yarn'"):
Connection(name="bad", master="yarn-cluster")
with pytest.raises(ValueError, match="must be"):
Connection(name="bad", master="http://rm:8088")
with pytest.raises(ValueError, match="must be"):
Connection(name="bad", master="")
def test_pending_submission_defaults_to_pending_status():
p = PendingSubmission(
pending_id="p_1",
app_name="test-app",
connection="prod",
master="yarn",
deploy_mode="cluster",
script_path="/tmp/j.py",
queue="default",
executor_memory="2G",
executor_cores=2,
num_executors=2,
spark_conf={},
created_at=datetime(2026, 6, 24),
)
assert p.status == "PENDING"
assert p.error is None
assert p.job_id is None
assert p.application_id is None
assert p.yarn_rm_url is None # default for connections without one set
assert p.app_name == "test-app"
def test_pending_submission_carries_yarn_rm_url_snapshot():
"""snapshotting yarn_rm_url lets prepare/confirm survive connection edits."""
p = PendingSubmission(
pending_id="p_x",
app_name="test-app",
connection="prod",
master="yarn",
deploy_mode="cluster",
yarn_rm_url="http://rm-prod:8088",
script_path="/tmp/j.py",
queue="default",
executor_memory="2G",
executor_cores=2,
num_executors=2,
spark_conf={},
extra_args={},
created_at=datetime(2026, 6, 24),
)
assert p.yarn_rm_url == "http://rm-prod:8088"
def test_pending_submission_can_record_outcome():
p = PendingSubmission(
pending_id="p_2",
app_name="test-app",
connection="prod",
master="yarn",
deploy_mode="cluster",
script_path="/tmp/j.py",
queue="default",
executor_memory="2G",
executor_cores=2,
num_executors=2,
spark_conf={},
extra_args={},
created_at=datetime(2026, 6, 24),
status="SUBMITTED",
job_id="j_abc",
application_id="application_17400000001",
)
assert p.status == "SUBMITTED"
assert p.job_id == "j_abc"
assert p.application_id == "application_17400000001"