mirror of
https://github.com/github/spec-kit.git
synced 2026-07-07 22:56:57 +08:00
feat(integration): add status reporting (#2674)
* feat(integration): add status reporting * docs(integration): include status in query command docstring * fix(integration): handle Windows extended-length paths in status containment On Windows, os.readlink() (and sometimes Path.resolve()) return paths with the \\?\ extended-length prefix. Comparing such a target against a plain project root via Path.relative_to() spuriously fails, so an in-project dangling symlink was classified as `invalid` instead of `missing` — failing test_status_treats_dangling_symlink_as_missing and the windows-style variant on the Windows CI runners. Centralize the containment check in _is_within_project() and strip the \\?\ / \\?\UNC\ prefix from both sides before relative_to(). Add portable regression tests for the prefix-stripping helper and the containment contract. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * test(integration): restore top-level pytest import after rebase A three-way merge / rebase onto main silently dropped the module-level `import pytest` from test_integration_subcommand.py: main reorganized the import block without it (using only a local `import pytest as _pytest`), while this branch added top-level fixtures and `pytest.skip`/`pytest.raises` usage. The overlapping import-hunk edits resolved by dropping the import, breaking collection with `NameError: name 'pytest' is not defined` on every runner. Re-add the import in the third-party group. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(integration): fix Windows UNC path assertion in status helper test `test_strip_extended_length_prefix_normalizes_windows_paths` compared the str() form of the helper's output against a hand-built string. On Windows, pathlib renders a UNC root with a trailing separator (`\\server\share\`), so the exact string match failed there (`\\server\share\` != `\\server\share`) even though `_strip_extended_length_prefix` behaves correctly — the trailing separator is irrelevant to the `relative_to` containment check it feeds. Compare Path objects (semantic equality) instead of exact strings so the assertion holds on both POSIX and Windows. No production code change needed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(integration): make shared-manifest remediation specify --integration The fallback `_manifest_suggestion` for the shared `speckit` manifest (used when no usable default integration is recorded) suggested `specify init --here --force`, which can trigger interactive integration selection. For CI/agent consumers of `integration status`, surface an explicit `--integration <key>` placeholder, matching the file's existing `<key>` suggestion style. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -25,17 +25,14 @@ class IntegrationReadError:
|
||||
schema: int | None = None
|
||||
|
||||
|
||||
def try_read_integration_json(
|
||||
def _read_integration_json_data(
|
||||
project_root: Path,
|
||||
) -> tuple[dict[str, Any] | None, IntegrationReadError | None]:
|
||||
"""Parse ``.specify/integration.json`` without raising.
|
||||
"""Read raw integration state without normalizing or raising.
|
||||
|
||||
Returns ``(normalized_state, None)`` on success, ``(None, None)`` when the
|
||||
file does not exist, or ``(None, error)`` for any parse / validation
|
||||
failure. This is the single low-level reader; both the CLI's loud
|
||||
``_read_integration_json`` and the workflow engine's silent
|
||||
``_load_project_integration`` consume it so the schema guard and parse
|
||||
logic cannot drift between them.
|
||||
Returns ``(data, None)`` when the JSON object is readable and supported,
|
||||
``(None, None)`` when the file is absent, and ``(None, error)`` for parse,
|
||||
schema, encoding, or filesystem failures.
|
||||
"""
|
||||
path = project_root / INTEGRATION_JSON
|
||||
# Avoid Path.exists() / Path.is_file() as a pre-check: both return False
|
||||
@@ -70,9 +67,41 @@ def try_read_integration_json(
|
||||
and schema > INTEGRATION_STATE_SCHEMA
|
||||
):
|
||||
return None, IntegrationReadError(kind="schema_too_new", schema=schema)
|
||||
return data, None
|
||||
|
||||
|
||||
def try_read_integration_json(
|
||||
project_root: Path,
|
||||
) -> tuple[dict[str, Any] | None, IntegrationReadError | None]:
|
||||
"""Parse ``.specify/integration.json`` without raising.
|
||||
|
||||
Returns ``(normalized_state, None)`` on success, ``(None, None)`` when the
|
||||
file does not exist, or ``(None, error)`` for any parse / validation
|
||||
failure. This helper delegates file I/O and raw JSON validation to
|
||||
``_read_integration_json_data`` so callers that need raw state can share
|
||||
the same low-level reader instead of duplicating parse logic.
|
||||
"""
|
||||
data, error = _read_integration_json_data(project_root)
|
||||
if data is None:
|
||||
return None, error
|
||||
return normalize_integration_state(data), None
|
||||
|
||||
|
||||
def try_read_integration_json_with_raw(
|
||||
project_root: Path,
|
||||
) -> tuple[dict[str, Any] | None, dict[str, Any] | None, IntegrationReadError | None]:
|
||||
"""Parse ``integration.json`` and return normalized plus raw state.
|
||||
|
||||
Returns ``(normalized_state, raw_state, None)`` when the file is readable,
|
||||
``(None, None, None)`` when it is absent, and ``(None, None, error)`` for
|
||||
parse, schema, encoding, or filesystem failures.
|
||||
"""
|
||||
data, error = _read_integration_json_data(project_root)
|
||||
if data is None:
|
||||
return None, None, error
|
||||
return normalize_integration_state(data), data, None
|
||||
|
||||
|
||||
def clean_integration_key(key: Any) -> str | None:
|
||||
"""Return a stripped integration key, or None for empty/non-string values."""
|
||||
if not isinstance(key, str) or not key.strip():
|
||||
|
||||
Reference in New Issue
Block a user