From 42e583a118eec060d937e290a37330bb9626d8b2 Mon Sep 17 00:00:00 2001 From: Speculator55005 <50082482+fas89@users.noreply.github.com> Date: Sat, 27 Jun 2026 06:56:18 +0200 Subject: [PATCH] docs+gate: correct fluid plugins examples + teach the docs-honesty gate about CLI commands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `fluid plugins list` example listed 4 providers under a "(5)" header (omitted `odps`) — verified against the real command, which prints aws/gcp/local/odps/snowflake. And your-first-real-plugin.md claimed PluginMetadata identity "surfaces to `fluid plugins list`" — it does not (the command reads entry-point names + allow/block status only and never loads the plugin). Both corrected. Extends tests/unit/test_docs_honesty.py (previously blind to CLI-command-example drift, which is how a fabricated table slipped through): - test_documented_cli_commands_exist: asserts every `fluid ` in a doc code fence is a real CLI subcommand, via argparse introspection. Runs only when the data-product-forge CLI is importable; SKIPS with a clear reason in CLI-less SDK CI ("validate where feasible"). Borrowed the argparse-introspection pattern; diverged from clitest/cram (which run the binary) since this zero-dep SDK doesn't install it. - test_fluid_plugins_output_advertises_no_impossible_columns: feasible WITHOUT the CLI — a documented `fluid plugins` block cannot advertise VERSION/AUTHOR/DESCRIPTION columns the command can't emit. Pins exactly the drift class this PR fixes. Verified both checks catch injected regressions (fabricated command + the old VERSION/AUTHOR/DESCRIPTION table) then go green. 156 tests pass (+1 skip in CLI-less CI). --- docs/walkthrough/plug-into-fluid-cli.md | 1 + docs/walkthrough/your-first-real-plugin.md | 4 +- tests/unit/test_docs_honesty.py | 97 ++++++++++++++++++++++ 3 files changed, 101 insertions(+), 1 deletion(-) diff --git a/docs/walkthrough/plug-into-fluid-cli.md b/docs/walkthrough/plug-into-fluid-cli.md index 398f5c9..1d6ff34 100644 --- a/docs/walkthrough/plug-into-fluid-cli.md +++ b/docs/walkthrough/plug-into-fluid-cli.md @@ -32,6 +32,7 @@ fluid plugins list • aws allowed • gcp allowed • local allowed + • odps allowed • snowflake allowed ``` diff --git a/docs/walkthrough/your-first-real-plugin.md b/docs/walkthrough/your-first-real-plugin.md index c13eefd..27d46ab 100644 --- a/docs/walkthrough/your-first-real-plugin.md +++ b/docs/walkthrough/your-first-real-plugin.md @@ -104,7 +104,9 @@ class GitLabCIScaffold(CustomScaffold): name = "gitlab-ci" - # ── identity (surfaces to `fluid plugins list`) ───────────── + # ── identity (descriptive metadata for registry / marketplace tooling) ── + # NB: these fields do NOT appear in `fluid plugins list` — that command reads + # entry-point names + allow/block status only and never loads the plugin. @classmethod def get_plugin_info(cls) -> PluginMetadata: diff --git a/tests/unit/test_docs_honesty.py b/tests/unit/test_docs_honesty.py index 591f431..8135821 100644 --- a/tests/unit/test_docs_honesty.py +++ b/tests/unit/test_docs_honesty.py @@ -47,6 +47,14 @@ ) _GROUP_RE = re.compile(r"fluid_build\.[a-z_]+") +# Markdown fenced code blocks, and the FIRST token of a `fluid ` invocation +# inside one (the top-level subcommand; flags are skipped by the `[a-z]` start). +_FENCE_RE = re.compile(r"```[\w]*\n(.*?)```", re.DOTALL) +_FLUID_CMD_RE = re.compile(r"(?m)^\s*\$?\s*fluid\s+([a-z][a-z0-9-]*)\b") +# Columns the `fluid plugins` command provably cannot emit — it reads entry-point +# names + allow/block status only and never loads plugin code to read PluginMetadata. +_IMPOSSIBLE_PLUGINS_COLUMNS = ("VERSION", "AUTHOR", "DESCRIPTION") + def _docs() -> list: files = [] @@ -55,6 +63,44 @@ def _docs() -> list: return sorted(f for f in files if f.is_file()) +def _fenced_blocks(text: str): + return _FENCE_RE.findall(text) + + +def _documented_fluid_commands() -> Set[Tuple[str, str]]: + """Return {(doc_relpath, command)} for every `fluid ` in a code fence.""" + out: Set[Tuple[str, str]] = set() + for doc in _docs(): + rel = str(doc.relative_to(_REPO_ROOT)) + for block in _fenced_blocks(doc.read_text(encoding="utf-8")): + for cmd in _FLUID_CMD_RE.findall(block): + out.add((rel, cmd)) + return out + + +def _cli_top_level_commands(): + """The CLI's real top-level subcommands, or ``None`` if the CLI isn't installed. + + The SDK is zero-dependency and its CI does not install the ``data-product-forge`` + CLI, so this returns ``None`` there and the command-existence check skips. + """ + import importlib.util + + if importlib.util.find_spec("fluid_build") is None: + return None + try: + from fluid_build.cli import build_parser + + parser = build_parser() + cmds: Set[str] = set() + groups = getattr(parser, "_subparsers", None) + for action in (groups._group_actions if groups else []): + cmds |= set(getattr(action, "choices", {}) or {}) + return cmds + except Exception: + return None + + def _clean_name(token: str) -> str: token = token.strip().strip("(),") # strip an `as alias` @@ -139,3 +185,54 @@ def test_documented_role_harnesses_exist_and_subclass_base(harness: str) -> None cls = getattr(t, harness, None) assert cls is not None, f"fluid_sdk.testing.{harness} is documented but missing" assert issubclass(cls, t.PluginTestHarness) + + +# ── CLI command-example honesty ─────────────────────────────────────── +# The import-only checks above are blind to CLI-command-example drift (a doc could +# show a `fluid ` that doesn't exist, or output a command can't produce). These +# two close that gap as far as is feasible for a CLI-less SDK CI. +# +# Borrowed: the argparse-introspection pattern (assert documented commands exist in +# the parser tree). We DIVERGE from clitest / cram / markdown-clitest (which run the +# binary) because this zero-dependency SDK does not — and should not — install the +# `data-product-forge` CLI in its test env. + + +def test_documented_cli_commands_exist() -> None: + """Every `fluid ` shown in a doc code fence is a real CLI command. + + Runs only when the CLI happens to be importable (e.g. a dev env with both + installed); skips with a clear reason in SDK-only CI — "validate where feasible". + """ + cli_cmds = _cli_top_level_commands() + if cli_cmds is None: + pytest.skip("data-product-forge CLI not installed; CLI-command-existence check skipped") + bad = sorted( + f"{doc}: `fluid {cmd}`" for doc, cmd in _documented_fluid_commands() if cmd not in cli_cmds + ) + assert not bad, "Docs reference `fluid` commands that do not exist:\n " + "\n ".join(bad) + + +def test_fluid_plugins_output_advertises_no_impossible_columns() -> None: + """A documented `fluid plugins` output block must not claim columns the command + cannot emit (VERSION / AUTHOR / DESCRIPTION). + + Feasible WITHOUT the CLI installed, so it runs in SDK-only CI too — it pins the + exact output-drift class (a NAME/ROLE/VERSION/AUTHOR/DESCRIPTION table) that the + import-only gate is structurally blind to. A real header row carries several of + these together, so we require >= 2 in one block to avoid flagging incidental prose. + """ + offenders = [] + for doc in _docs(): + text = doc.read_text(encoding="utf-8") + if "fluid plugins" not in text: + continue + rel = str(doc.relative_to(_REPO_ROOT)) + for block in _fenced_blocks(text): + present = [c for c in _IMPOSSIBLE_PLUGINS_COLUMNS if c in block] + if len(present) >= 2: + offenders.append(f"{rel}: a `fluid plugins` block advertises {present} columns") + assert not offenders, ( + "`fluid plugins` reads entry-point names + allow/block status only — it cannot " + "print plugin version/author/description:\n " + "\n ".join(sorted(set(offenders))) + )