Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions plugins/nemo-eval-author/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,13 @@ dependencies = [
"nemo-experimentalist-plugin",
"nemo-insights-plugin",
"nemo-platform",
"nemo-platform-plugin",
"tomlkit>=0.13.3",
]

[project.entry-points."nemo.cli"]
eval-author = "nemo_eval_author_plugin.cli:EvalAuthorCLI"

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
Expand Down
73 changes: 73 additions & 0 deletions plugins/nemo-eval-author/src/nemo_eval_author_plugin/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

"""Eval Author plugin CLI — ``nemo eval-author ...`` subcommands.

Scaffolding. Every verb is registered under its final name so the command tree is
discoverable and the child tickets have a landing spot, and each body exits non-zero
until its own ticket lands. Flags belong to those tickets, so nothing here declares
options yet.

This module imports nothing from ``eval_author``: those agents build their LLM client
while the class body executes, so importing one here would make the whole CLI require
``AUTHOR_*`` credentials — including the ``doctor`` verb whose job is to report that they
are missing. Experimentalist's CLI keeps its runner behind a lazily-assigned module
global for the same reason.
"""

from typing import ClassVar, NoReturn

import typer
from nemo_platform_plugin.cli import NemoCLI


def _not_implemented(command: str, ticket: str) -> NoReturn:
"""Fail loudly, so a placeholder verb can never be mistaken for a successful run."""
typer.echo(f"`nemo eval-author {command}` is not implemented yet ({ticket}).", err=True)
raise typer.Exit(code=1)


class EvalAuthorCLI(NemoCLI):
"""``nemo eval-author ...`` subcommands."""

name: ClassVar[str] = "eval-author"
description: ClassVar[str] = "NeMo Eval Author commands."

def get_cli(self) -> typer.Typer:
app = typer.Typer(help=self.description, no_args_is_help=True)

@app.callback()
def _root() -> None:
"""Force subcommand dispatch even when only one verb is registered."""

@app.command("discover")
def discover() -> None:
"""Discover candidate evaluation cases from agent traces."""
# TODO(ASE-677): declare flags and wire discovery.
_not_implemented("discover", "ASE-677")

@app.command("audit")
def audit() -> None:
"""Audit an existing eval suite for coverage gaps."""
# TODO(ASE-676): declare flags and wire the audit.
_not_implemented("audit", "ASE-676")

@app.command("propose")
def propose() -> None:
"""Propose eval suite additions for review."""
# TODO(ASE-675): declare flags and wire the proposal.
_not_implemented("propose", "ASE-675")

@app.command("run")
def run() -> None:
"""Run the Eval Author pipeline end to end."""
# TODO(ASE-673): declare flags and wire the pipeline to run_eval_author.
_not_implemented("run", "ASE-673")

@app.command("doctor")
def doctor() -> None:
"""Diagnose Eval Author setup: credentials, platform, runtime."""
# TODO(ASE-678): report the prerequisites the other verbs gate on.
_not_implemented("doctor", "ASE-678")

return app
64 changes: 64 additions & 0 deletions plugins/nemo-eval-author/tests/test_cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

"""Scaffolding tests: the command tree exists, and every verb still refuses to run.

The entry-point cases cover the ``pyproject.toml`` wiring that nothing else exercises. A
typo in the key or the import path does not fail an import; it just makes ``nemo
eval-author`` quietly missing from the CLI, which no unit test of this module would catch.
"""

from importlib.metadata import EntryPoint, entry_points

import pytest
import typer
from nemo_eval_author_plugin import cli
from typer.testing import CliRunner

runner = CliRunner()

# Each verb is a placeholder owned by the child ticket named beside it.
_PLACEHOLDER_VERBS = [
("discover", "ASE-677"),
("audit", "ASE-676"),
("propose", "ASE-675"),
("run", "ASE-673"),
("doctor", "ASE-678"),
]


@pytest.fixture
def app() -> typer.Typer:
return cli.EvalAuthorCLI().get_cli()


def _eval_author_entry_point() -> EntryPoint:
matches = [entry for entry in entry_points(group="nemo.cli") if entry.name == "eval-author"]
assert matches, "no nemo.cli entry point named 'eval-author'; reinstall the plugin with uv sync"
return matches[0]


def test_help_lists_every_verb(app: typer.Typer) -> None:
result = runner.invoke(app, ["--help"])

assert result.exit_code == 0, result.output
for command, _ in _PLACEHOLDER_VERBS:
assert command in result.output


@pytest.mark.parametrize(("command", "ticket"), _PLACEHOLDER_VERBS)
def test_verb_refuses_to_run_and_names_its_ticket(app: typer.Typer, command: str, ticket: str) -> None:
result = runner.invoke(app, [command])

assert result.exit_code == 1, result.output
assert ticket in result.output


def test_entry_point_key_matches_the_cli_name() -> None:
"""Discovery rejects a plugin whose entry-point key differs from its ``name``."""
assert _eval_author_entry_point().value == "nemo_eval_author_plugin.cli:EvalAuthorCLI"
assert cli.EvalAuthorCLI.name == "eval-author"


def test_entry_point_loads_the_cli_class() -> None:
assert _eval_author_entry_point().load() is cli.EvalAuthorCLI
9 changes: 3 additions & 6 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading