SuperAPI AI Directory

clak

Testing & quality

View repository

Updated 10-15 21:52

About this skill

clak Business Script Development

Use clak to build fixed-process business tools. Business code creates its own Clak instance and registers Script subclasses; the global clak command contains only built-in commands and cannot run business scripts.

Installation

Running and testing this skill's examples all require the following dependencies:

python -m pip install esclak pytest

Production projects that do not run pytest can install only esclak; however, the commands in the "Testing" and "Pre-commit Checks" sections of this document require installing pytest。

Skill Built-in Reference Implementations

This skill is self-contained with copyable reference code. Read the paths relative to this file as needed:

  • references/dev_tools/README.md: complete usage and run commands
  • references/dev_tools/app.py: app, status bar, and footer registration
  • references/dev_tools/__main__.py: project-level CLI entry point
  • references/dev_tools/scripts/select_res_server.py: filtering selection and shared state
  • references/dev_tools/scripts/pack_upload.py: multi-step orchestration, subprocesses, conditional steps, and resource cleanup
  • references/dev_tools/scripts/verify_release.py: business failures and exit codes
  • references/testing.py:ScriptTester and ScriptRun test fixtures

Do not import the reference project as a dependency. Copy its structure and patterns into the business project; the target project only needs to install the esclak package to use this skill.

Minimal Project Structure

my_tools/
├── __init__.py
├── __main__.py
├── app.py
└── scripts/
    ├── __init__.py
    └── deploy.py
# app.py
from clak import Clak
from .scripts.deploy import DeployScript

app = Clak(status="发布工具 · staging")
app.register(DeployScript)
# __main__.py
from .app import app

app.run_cli(prog="my-tools")

The business entry point must be:

python -m my_tools
python -m my_tools exec /deploy

Do not run clak exec /deploy, as it will not load scripts registered by the business project.

Script Conventions

Each script run creates a new instance.run takes no arguments; the framework binds the session in on_enter(session) on completion, failure, or cancellation. on_exit()。

from clak import Script


class DeployScript(Script):
    name = "/deploy"
    description = "发布服务"

    def run(self) -> None:
        def deploy() -> int:
            self.output.push("发布完成")
            return 0

        self.add_step(True, deploy, "deploy")
        self.run_steps()
  • name must be a slash command, such as /deploy
  • run(self) defines and orchestrates the steps for this execution
  • self.add_step(can_do, task, name) adds steps,can_do is a boolean or a no-argument predicate function
  • Steps return 0 to continue; non- 0 means waiting for input and then resuming, not a business failure
  • End self.run_steps() with run; do not directly manipulate the Session execution queue
  • self.output.push() automatically adds the [script-name] prefix
  • Throw ScriptError(message, exit_code=1)for invalid argument formats ValueError

Collecting Input

Input steps return self.ask_tool(request, on_answer); the answer callback saves business state; the next step continues after the input is submitted.

from clak import AskAnswer, AskChoice, AskMode, AskRequest


def ask_environment() -> int:
    def save(answer: AskAnswer) -> None:
        state.environment = answer.primary
        self.output.push(f"环境:{state.environment}")

    return self.ask_tool(
        AskRequest(
            field_id="environment",
            prompt="选择环境",
            default="staging",
            choices=(
                AskChoice(label="预发", value="staging"),
                AskChoice(label="生产", value="production"),
            ),
            mode=AskMode.SINGLE,
        ),
        save,
    )

AskRequest.default is esclak a required constructor parameter ofin the current API, and must be passed explicitly even if the business does not wish to provide a usable default, for example default="". Only non-empty defaults can be automatically adopted by the headless CLI; empty defaults fail just like missing input.

The actual input rules for the headless CLI are as follows:

Command Form Behavior for Unprovided ask Fields
Without any --set, such as python -m my_tools exec /deploy Automatically adopt each field's non-empty AskRequest.default. When no usable default exists, the exit code is 2。
With --set environment=staging That field uses the explicit value. If there are still unprovided fields, the default valuewill notbe automatically adopted; the exit code for missing fields is 2。
With --set ... --use-defaults Explicit --set takes precedence; the remaining fields use their respective non-empty default values.

Therefore, when designing headless batch processing, declare defaults for safe default paths; for fields that must be confirmed by the caller, pass default="", and use --set to provide it.--use-defaults is not "skipping validation": the framework will still validate the choice, and the answer callback should still validate the business format.

  • field_id is the --set field_id=value key for headless mode
  • AskMode.SINGLE single-select,MULTI multi-select,FILTER filterable select
  • Only AskChoice(value=None) allows single-select fields to accept text outside the candidates; the callback must still validate the business format
  • Headless run:python -m my_tools exec /deploy --set environment=staging

Resources, state, and error boundaries

  • Temporary directories, file handles, and external resources are created in on_enter and cleaned up in on_exit ; cleanup must cover completion, failure, and cancellation
  • In-process state across scripts goes into an explicit context.py, and provide reset() for test isolation
  • For invoking external commands use subprocess.run(..., check=False, timeout=...), converting startup exceptions and non-zero exit codes into ScriptError
  • Validate paths, user input, and permission parameters first; do not mistakenly write a failure exit code as a step hang

Testing

Prefer using ScriptTester to verify the script lifecycle, waiting for input, and the success and failure paths.

from clak import ScriptTester
from my_tools.scripts.deploy import DeployScript


def test_deploy_asks_then_completes():
    run = ScriptTester(DeployScript).run()
    assert run.is_awaiting("environment")

    run.provide("environment", "staging")

    assert run.succeeded
    assert run.output.contains("发布完成")

Each script should cover at least:

  1. The success path with default or complete input
  2. The behavior when each required input is missing
  3. Invalid candidates or format errors return exit code 2
  4. ScriptError Business failures return the expected exit code
  5. When there are resources, cleanup is performed on completion, failure, and cancellation
  6. Execution and skipping of conditional steps

Finally run:

python -m pytest
python -m my_tools exec /deploy --set environment=staging

Pre-commit checks

  • The business entry point, not the global clak command, is runnable
  • run(self) has called self.run_steps()
  • The output contains locatable business information
  • The exit codes for headless success, business failure, and parameter failure are correct respectively
  • TUI manual testing of search, input, cancellation, and dynamic state or footer