Skip to content

fix: review hardening — parameter assembly, template quoting, store writes#26

Merged
t41372 merged 3 commits into
mainfrom
fix/review-hardening
Jul 22, 2026
Merged

fix: review hardening — parameter assembly, template quoting, store writes#26
t41372 merged 3 commits into
mainfrom
fix/review-hardening

Conversation

@t41372

@t41372 t41372 commented Jul 22, 2026

Copy link
Copy Markdown
Owner

Two commits: a batch of review fixes (a1c9ec7), then the repairs for the regressions that batch itself introduced (abf7f83).

Why the second commit exists

a1c9ec7 was reviewed by three independent passes, and every finding was then re-verified against the real code with a reproduction before anything was changed. Ten defects survived that verification — four of them regressions introduced by a1c9ec7, the rest pre-existing. Two reported findings were rejected and are recorded in the commit message with the reasoning.

What the second commit fixes

Template quoting. The context-aware quoter added in a1c9ec7 tracked a single quote character, but a command substitution restarts quoting. A value in "$(cmd {v})" was escaped for double quotes and reached the nested command bare (;/| live, a b split into two words); a value in "$(cmd "{v}")" was shlex-quoted and arrived wrapped in literal apostrophes. The state is now a frame stack. Double quotes nested in a backtick substitution are refused rather than guessed — that form strips one layer of backslashes before parsing, so the \$ guard arrives bare.

input shadowing is resolved per scope. a1c9ec7 reused a constant-folding predicate that counts bindings file-wide. For folding, an extra binding just skips a fold; for injection it meant one def sanitize(input) anywhere disabled the whole script and turned already-stored parameters into hard drift errors on the next run.

Parameter assembly. nargs=N>1 is a multi-value field for argparse and click alike, and typed multi-value fields validate per piece (--point 1 2 was rejected as "not a whole number", then assembled as one quoted token). click's multiple=True, nargs=N>1 has no representable shape and goes to the passthrough field. fish's =+/=* gained the repeat axis the other readers got. A bool flag that is on by default is refused instead of being stamped store_true, which made its unticked checkbox deliver nothing.

Writes that must not lie. Script settings saved a lossily-read string back as strict UTF-8, so one checkbox tick flattened a CRLF script and burned every non-UTF-8 byte to U+FFFD. skit deps --clear on a non-UTF-8 copy carrying its own PEP 723 block reported success while uv kept installing the old list — now refused before meta is written. atomic_write_text_keep_mode fchmods before the rename so the mode is part of the atomic swap. store.remove's partial-deletion error now reaches the user instead of a traceback (CLI) or a crash out of the terminal (TUI).

PEP 420. A bare sibling directory is a namespace portion and never shadows an installed package, so a docs/ or assets/ folder colliding with an import name no longer drops a real dependency.

Verification

ruff format / ruff check / ty clean, 4969 tests pass, 100% coverage, i18n gate passes with both catalogs at 100%. Targeted mutation testing over all 19 changed functions went from 23 surviving mutants to zero; the one genuine equivalent mutant carries a pragma explaining why.

Summary by CodeRabbit

  • New Features

    • Improved support for repeatable and multi-value command-line options (including new repeat details in show --json form fields).
    • Dependency suggestions now exclude local modules and packages.
  • Bug Fixes

    • Safer shell/template placeholder substitution with improved POSIX quoting correctness.
    • Preserves file permissions, original line endings, and non-UTF-8 content during edits and settings saves.
    • Prevents concurrent parameter-state updates from overwriting each other.
    • Clearer errors for failed removals and non-runnable paths; avoids treating shadowed Python input() as managed prompts.
  • Documentation

    • Clarified that secret parameters are excluded from remembered values, presets, and run history (EN + updated translations).

t41372 added 2 commits July 22, 2026 02:29
A batch of correctness and robustness fixes found in review.

Parameters & delivery:
- Repeated options (click multiple / parseArgs multiple) assemble as
  `--tag a --tag b` instead of the argparse-style `--tag a b`, via a new
  `repeat` axis on ParamDecl/FormField; the click and JS readers set it.
- Declared bool flags with an empty action default to store_true, so the
  checkbox is no longer a silent no-op; a flagless bool never emits an
  empty-string argv element.

Command templates & exe launch:
- Placeholders are quoted for their actual shell context, so a value in a
  "double-quoted" slot no longer leaves $(...) live; a dangling backslash
  before a placeholder can no longer re-arm the escape.
- A directory (or other non-regular file) given as an exe is refused with a
  clean 126 instead of crashing subprocess.

Store, state & atomicity:
- Every argstate read-modify-write holds a per-slug advisory lock, closing a
  last-writer-wins data-loss window.
- Settings and dependency writes are atomic and mode-preserving; the PEP 723
  sync no longer re-encodes a non-UTF-8 stored copy into U+FFFD.
- Rename uniqueness is re-checked under the registry lock; remove reports when
  the entry files could not be fully deleted.

Analysis & inference:
- Sibling local modules are no longer suggested as PyPI dependencies.
- A script that shadows `input` is not treated as calling the builtin, for both
  the analyzer and the injection shim.

CI & docs:
- Skill markdown edits (skills/**, src/skit/skills/**) still trigger CI.
- Correct the "secrets never touch disk" wording in all three READMEs and
  refresh the i18n catalogs.
Three independent reviews (plus a verification pass on each finding) went over
a1c9ec7. Ten defects survived verification; each was reproduced before it was
fixed, and the two that did not survive are recorded at the end.

Template quoting:
- `_posix_quote_state` tracks a FRAME STACK, not one quote character. A command
  substitution restarts quoting, so a placeholder inside `"$(cmd {v})"` sits in
  the nested command's own unquoted context — it was being escaped for double
  quotes, leaving `;`/`|` live and splitting `a b` into two words — while one
  inside `"$(cmd "{v}")"` was shlex-quoted and arrived wrapped in literal
  apostrophes. Backticks are frames too.
- Double quotes nested in a backtick substitution are refused rather than
  guessed: that form strips one layer of backslashes before parsing the inner
  command, so the `\$` guard arrives bare. `$(…)` has no such layer.

Analysis:
- `input` shadowing is resolved PER SCOPE. The file-wide predicate came from
  constant folding, where an extra binding merely skips a fold; here it meant a
  single `def sanitize(input)` anywhere disabled injection for the whole script
  and turned stored parameters into hard drift errors on the next run.
- A sibling directory only counts as a local module when it holds Python. PEP
  420 makes a bare directory a namespace *portion*, which never shadows an
  installed regular package — so a `docs/` or `assets/` folder colliding with an
  import name was silently dropping a real dependency.

Parameter assembly:
- `nargs=N>1` is a multi-value field for both argparse and click, and a typed
  multi-value field validates each piece — `--point 1 2` was rejected as "not a
  whole number" and then assembled as one quoted token.
- click's `multiple=True, nargs=N>1` has no representable shape at all, so the
  option goes to the passthrough field instead of an invalid `--point 1 --point 2`.
- fish's `=+`/`=*` set the `repeat` axis, which had been applied per-reader.
- A declared bool flag that is ON by default is refused: its flag can only turn
  it on again, so store_true made the unticked checkbox deliver nothing.

Writes that must not lie:
- Script settings saves are byte-lossless. The screen read its copy with
  errors="replace" and universal newlines and wrote that back as strict UTF-8,
  so one checkbox tick flattened a CRLF script and burned every non-UTF-8 byte
  to U+FFFD. `detect_newline`/`restore_newline` move to rewrite.py, which both
  callers now share.
- `skit deps --clear` / `--python -` on a non-UTF-8 copy that carries its own
  PEP 723 block is refused before meta is written. meta cannot express a clear
  (an empty value means "defer to the block"), so the edit reported success
  while uv kept installing the old list.
- `atomic_write_text_keep_mode` fchmods the temp file before the rename, making
  the mode part of the atomic swap instead of a follow-up a crash can skip.
- `store.remove`'s partial-deletion error reaches the user: the CLI prints it
  and exits 1 instead of a traceback, and the TUI shows it in the status line
  instead of crashing out of the terminal.

Not changed, deliberately:
- `argstate.forget()` taking no lock — the reported race is not closed by one.
- `script_dir` on the copy-mode add path — the local-module filter it feeds is
  what stops `--no-input` installing an unrelated same-named PyPI package. The
  real gap is that copy mode stores a single file.

Verification: 100% coverage, and targeted mutation testing over all 19 changed
functions went from 23 surviving mutants to none.
@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: a81db222-122f-4b38-9c47-42de0966713f

📥 Commits

Reviewing files that changed from the base of the PR and between abf7f83 and d4bc7b6.

📒 Files selected for processing (5)
  • src/skit/store.py
  • tests/test_atomic.py
  • tests/test_store_fix.py
  • tests/test_store_mut.py
  • tests/test_tui_settings_cov.py

📝 Walkthrough

Walkthrough

The PR synchronizes state writes, preserves file modes and newline styles, expands repeated-option modeling, improves Python analysis and dependency discovery, hardens launch behavior, handles cleanup errors, and refreshes documentation, CI filters, translations, and regression coverage.

Changes

Skit behavior updates

Layer / File(s) Summary
State, storage, and permission-safe writes
src/skit/argstate.py, src/skit/atomic.py, src/skit/store.py, src/skit/tui*.py, tests/test_argstate_mut.py, tests/test_atomic.py, tests/test_store_*.py, tests/test_tui_*.py
Per-slug locks serialize state mutations; atomic writes preserve modes; copy-mode synchronization handles invalid UTF-8; rename, removal, CLI, and TUI failure paths are hardened.
Repeated options and boolean parameter contracts
src/skit/flows.py, src/skit/params.py, src/skit/cli.py, src/skit/langs/*/cli_reader.py, tests/test_flows.py, tests/test_argspec*.py, tests/test_show.py
Repeat metadata, fixed-arity options, boolean actions, per-piece validation, flag assembly, and JSON output are updated and tested.
Python analysis and dependency suggestions
src/skit/langs/python/*, src/skit/pep723.py, src/skit/cli.py, src/skit/tui_add.py, tests/test_analyzer.py, tests/test_shim.py, tests/test_phase1.py
Builtin input() detection ignores shadowed bindings, while dependency suggestions exclude local sibling modules when script context is available.
Executable checks and shell-aware template rendering
src/skit/langs/launch.py, tests/test_launch_mut.py, tests/test_launcher.py, tests/test_template_context_quoting.py
Non-file executable sources are rejected and POSIX placeholder substitution tracks quote and command-substitution contexts before escaping values.
Newline-preserving script edits
src/skit/rewrite.py, src/skit/cli.py, src/skit/store.py, src/skit/tui_settings.py, tests/test_rewrite.py, tests/test_store_fix.py, tests/test_tui_settings_cov.py
Editing paths detect and restore original newline styles while preserving surrogateescaped bytes and file modes.
CI, documentation, and translation catalogs
.github/workflows/ci.yml, README*.md, src/skit/locales/*
CI path filters, secret-parameter documentation, and generated gettext catalogs are refreshed.

Estimated code review effort: 5 (Critical) | ~120 minutes

Poem

A rabbit found locks in the state-file door,
And flags now repeat their values once more.
Shell quotes hop safely, local imports stay,
Newlines keep dancing the old-fashioned way.
“CI,” said the bunny, “now runs just right!”

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 24.35% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately summarizes the main areas changed in the pull request.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/test_store_fix.py`:
- Around line 548-569: Hoist the duplicated Path.read_bytes OSError mock used by
test_update_dependencies_copy_sync_swallows_read_oserror and the corresponding
tests into a shared pytest fixture in conftest.py, such as
unreadable_named_path(monkeypatch). Update each test to use the fixture with its
target filename, preserving the existing simulated error behavior and
assertions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 3f6b6398-4181-402d-bdc4-976b43f11cf8

📥 Commits

Reviewing files that changed from the base of the PR and between 7f53c5e and abf7f83.

📒 Files selected for processing (53)
  • .github/workflows/ci.yml
  • README.md
  • README.zh-CN.md
  • README.zh-TW.md
  • src/skit/argstate.py
  • src/skit/atomic.py
  • src/skit/cli.py
  • src/skit/flows.py
  • src/skit/langs/fish/cli_reader.py
  • src/skit/langs/javascript/cli_reader.py
  • src/skit/langs/launch.py
  • src/skit/langs/python/analyzer.py
  • src/skit/langs/python/argspec.py
  • src/skit/langs/python/shim.py
  • src/skit/locales/skit.pot
  • src/skit/locales/zh_CN/LC_MESSAGES/skit.mo
  • src/skit/locales/zh_CN/LC_MESSAGES/skit.po
  • src/skit/locales/zh_TW/LC_MESSAGES/skit.mo
  • src/skit/locales/zh_TW/LC_MESSAGES/skit.po
  • src/skit/params.py
  • src/skit/pep723.py
  • src/skit/rewrite.py
  • src/skit/store.py
  • src/skit/tui.py
  • src/skit/tui_add.py
  • src/skit/tui_settings.py
  • tests/test_add_feedback_contracts.py
  • tests/test_analyzer.py
  • tests/test_argspec.py
  • tests/test_argspec_click_typer.py
  • tests/test_argstate_mut.py
  • tests/test_atomic.py
  • tests/test_cli_mut_part04.py
  • tests/test_fish.py
  • tests/test_flows.py
  • tests/test_js_analyzer.py
  • tests/test_js_cli_reader_mut.py
  • tests/test_launch_mut.py
  • tests/test_launcher.py
  • tests/test_params_edit.py
  • tests/test_params_model.py
  • tests/test_pep723_mut.py
  • tests/test_phase1.py
  • tests/test_py_analyzer_mut.py
  • tests/test_rewrite.py
  • tests/test_settings_and_draft_review_atomicity.py
  • tests/test_shim.py
  • tests/test_show.py
  • tests/test_store_fix.py
  • tests/test_store_mut.py
  • tests/test_template_context_quoting.py
  • tests/test_tui_core_cov.py
  • tests/test_tui_settings_cov.py

Comment thread tests/test_store_fix.py
Comment on lines +548 to +569
def test_update_dependencies_copy_sync_swallows_read_oserror(tmp_path, monkeypatch):
"""The sync's guard covers OSError too, not just decode failures: if the stored copy can't be
read back at all, the edit still lands in meta and the sync degrades silently rather than
crashing the whole update."""
from skit import store

src = tmp_path / "plain.py"
src.write_text("print(1)\n", encoding="utf-8")
entry = store.add_python(src)

real_read_bytes = Path.read_bytes

def boom(self):
if self.name == "script.py":
raise OSError("simulated: unreadable stored copy")
return real_read_bytes(self)

monkeypatch.setattr(Path, "read_bytes", boom)
updated = store.update_dependencies(entry.slug, ["httpx"]) # must not crash

assert updated.meta.dependencies == ["httpx"]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Duplicated boom/monkeypatch helper across test files.

The boom OSError-simulation helper (lines 560-566) is byte-identical to the same helper already added in tests/test_atomic.py, tests/test_flows.py, tests/test_store_mut.py, and tests/test_tui_core_cov.py (all at the same line range 560-563). Consider hoisting this into a shared conftest.py fixture (e.g., unreadable_script_path(monkeypatch)) to avoid five copies of the identical mock diverging over time.

♻️ Example shared fixture
# conftest.py
import pytest
from pathlib import Path

`@pytest.fixture`
def unreadable_named_path(monkeypatch):
    real_read_bytes = Path.read_bytes

    def _apply(target_name: str):
        def boom(self):
            if self.name == target_name:
                raise OSError(f"simulated: unreadable {target_name}")
            return real_read_bytes(self)

        monkeypatch.setattr(Path, "read_bytes", boom)

    return _apply
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_store_fix.py` around lines 548 - 569, Hoist the duplicated
Path.read_bytes OSError mock used by
test_update_dependencies_copy_sync_swallows_read_oserror and the corresponding
tests into a shared pytest fixture in conftest.py, such as
unreadable_named_path(monkeypatch). Update each test to use the fixture with its
target filename, preserving the existing simulated error behavior and
assertions.

Windows CI caught a data-corrupting regression from a1c9ec7 that all three
review passes missed — none of them ran on Windows.

a1c9ec7 switched `_sync_python_block` from `read_text` (universal newlines) to
`read_bytes().decode()` to stop a lossy re-encode. That also stopped folding
CRLF, and the block engine's fences match on "\n" only. On a CRLF copy —
the default for anything Windows wrote — `parse_block` therefore found nothing,
so `skit deps` PREPENDED a second `# /// script` block instead of updating the
one already there, and every `[tool.skit]` parameter below it became
unreadable. Reproduced end to end: two stacked blocks, `read_params` returning
[] where it had returned the managed list.

`add_python` had the same hole on its own lane: `has_block` on unfolded CRLF
text said False, so a script that already declared its dependencies got a
second block injected on top.

Both sites now fold for the engine and restore the copy's own line endings on
the way out — the discipline `cli._edit_params` already followed, through the
`detect_newline`/`restore_newline` pair that moved to rewrite.py earlier in this
branch. The injected write also moves from `write_text` to `write_bytes`, since
`write_text` re-expands every "\n" to os.linesep and would rewrite an LF
script's whole file to CRLF on Windows for the sake of a comment block.

Regression tests cover all three directions (CRLF deps edit keeps one block and
its params, CRLF add does not double-block, LF add stays LF). Nothing covered
any of them before, which is why it shipped.

Also: four permission assertions hardcoded 0o755, which Windows cannot
represent — it reports 0o666 whatever it is handed. They now capture what chmod
actually produced and assert preservation, which is the contract under test.
@t41372

t41372 commented Jul 22, 2026

Copy link
Copy Markdown
Owner Author

Duplicated boom/monkeypatch helper across test files

Declining — the premise doesn't hold. The five boom functions are not byte-identical, and they aren't the same mock:

file signature patch target
test_store_fix.py:423 boom(self, *a, **kw) Path.write_bytes
test_store_fix.py:562 boom(self) Path.read_bytes
test_atomic.py (×3) boom(*_a: object, **_k: object) -> None shutil.copy2, os.fchmod, os.chmod
test_flows.py (×3) boom(*a, **k) (form/launch seams)
test_store_mut.py:824 boom(_src, _dst) shutil.rmtree
test_tui_core_cov.py:351 boom(path, *, kind) an editor seam

They also aren't at "the same line range 560-563" in each file. boom is just the conventional local name for "the callable that raises" in this suite, the way _py is the conventional local name for "write a script to tmp_path". A shared fixture would have to abstract over five different signatures and five different patch targets to unify functions whose only common feature is the word raise — that couples unrelated tests without removing any real duplication.

@t41372

t41372 commented Jul 22, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@t41372
t41372 merged commit a9c811a into main Jul 22, 2026
13 checks passed
@t41372
t41372 deleted the fix/review-hardening branch July 22, 2026 22:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant