Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
Loading
Loading