Skip to content

Create missing storage directory and allow quitting early during project creation - #48

Open
dmccoystephenson wants to merge 4 commits into
mainfrom
feat/create-flow-robustness
Open

Create missing storage directory and allow quitting early during project creation#48
dmccoystephenson wants to merge 4 commits into
mainfrom
feat/create-flow-robustness

Conversation

@dmccoystephenson

Copy link
Copy Markdown
Member

Summary

Two rough edges in the project-creation path are addressed.

  • Storage directory (Create directory for JSON storage if it doesn't already exist #32)writeJson passed the configured path straight to Jackson, so a projects.file pointing anywhere other than an existing directory failed with FileNotFoundException. The parent directory is now created before the write. Because mkdirs() returns false both when the directory already exists and when creation genuinely failed, an isDirectory() check distinguishes the two so only a real failure raises. This also removes the prerequisite that would otherwise block Save data to user's home directory by default #33 (defaulting storage to the user's home directory).
  • Quitting early (Allow user to quit early during project creation #37) — once create began prompting, no exit existed short of killing the shell, and an exhausted input stream (Ctrl-D) left the score loops re-prompting forever, since a null answer fails to parse and is retried. Interactive answers now pass through a readInput helper that treats q, quit (either case, surrounding whitespace ignored) and end-of-input as a request to stop; creation unwinds to a single Project creation cancelled. message with nothing persisted.
  • A one-line hint is printed before the first prompt, because the prompt text itself lives in application.yaml and is user-configurable, so it cannot be relied on to advertise the option. application.yaml is deliberately left untouched.
  • The five byte-identical score-retry loops collapse into a single promptForScore helper, so the cancellation path is threaded through one place rather than five. This refactor is included because duplicating the new control flow five times was the alternative, not as independent cleanup.

Test plan

  • ./gradlew test — 94 tests, 0 failures (90 before this branch; 4 added)
  • Regression evidence gathered empirically by stashing the two production files and re-running the new tests: writeJson_WhenParentDirectoryIsMissing_ShouldCreateIt, testQuittingAtNamePromptCancelsCreation and testEndOfInputCancelsCreation all failed; testQuittingAtScorePromptCancelsCreation never terminated (the pre-fix re-prompt loop), and completes in milliseconds with the fix restored.
  • README.md updated to document the cancel behaviour; CONTRIBUTING.md re-checked and still accurate.

Deferred backlog

The remaining open issues were not selected this cycle, with reasons recorded here for auditability:

Closes #32
Closes #37

This PR description was drafted during a Gardener session (https://github.com/Stephenson-Software/gardener).


drafted by Claude on behalf of Daniel Stephenson

dmccoystephenson and others added 2 commits August 31, 2026 01:07
writeJson handed the configured path straight to ObjectMapper, which
fails with a FileNotFoundException when the parent directory does not
exist. Any storage location other than the working directory therefore
required the user to create the directory by hand first.

The parent directory is now created before the write. mkdirs() returns
false both when the directory already exists and when creation genuinely
failed, so an isDirectory() check distinguishes the two and only a real
failure raises.

Closes #32

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Once `create` started prompting there was no way out short of killing
the shell: every prompt insisted on an answer, and an exhausted input
stream (Ctrl-D) left the score loops re-prompting forever because a null
answer fails to parse and is retried.

Interactive answers now go through a readInput helper that treats `q`,
`quit` (either case, surrounding whitespace ignored) and the end of the
input stream as a request to stop, unwinding to a single
"Project creation cancelled." message with nothing persisted. A one-line
hint is printed before the first prompt, since the prompt text itself is
user-configurable and cannot advertise this.

The five identical score-retry loops collapse into promptForScore so the
cancellation path is threaded through one place rather than five.

Closes #37

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Aug 31, 2026

Copy link
Copy Markdown

Test Results

0 tests   - 90   0 ✅  - 90   0s ⏱️ -1s
0 suites  - 14   0 💤 ± 0 
0 files    - 14   0 ❌ ± 0 

Results for commit 92353e3. ± Comparison against base commit 93d7c45.

♻️ This comment has been updated with latest results.

dmccoystephenson and others added 2 commits August 31, 2026 01:11
… answers

Two problems surfaced while reviewing the quit-early change.

The README now promises that Ctrl-D cancels creation, but that only held
when a real console was attached. The no-console fallback used by IDEs
and by a piped stdin calls Scanner.nextLine(), which throws
NoSuchElementException on an exhausted stream rather than returning null,
so exhausted input escaped as an unhandled exception instead of a clean
cancellation. The fallback now reports EOF as null, matching what
Console.readLine() returns, and gains its first test class.

readInput also returned the trimmed answer, quietly changing how every
existing answer was interpreted. Trimming is now confined to the quit
comparison and the answer itself is returned untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@dmccoystephenson

Copy link
Copy Markdown
Member Author

Self-review rubric

  • Scope: PASS with a noted exception — six of the seven files map onto Create directory for JSON storage if it doesn't already exist #32 or Allow user to quit early during project creation #37. The seventh, src/main/java/com/preponderous/parpt/util/SystemConsoleInputProvider.java, was pulled in by this review (see the first finding below) because the PR documents Ctrl-D as a cancel and that promise was false on one of the two input paths. The collapse of the five score-retry loops into promptForScore is included because the alternative was duplicating the new cancellation control flow five times, not as independent cleanup.
  • Tests-new: PASSreadInput and promptForScore are private and are covered through execute by testQuittingAtNamePromptCancelsCreation, testQuittingAtScorePromptCancelsCreation and testEndOfInputCancelsCreation; createParentDirectory by writeJson_WhenParentDirectoryIsMissing_ShouldCreateIt; the changed SystemConsoleInputProvider.readLine by the new SystemConsoleInputProviderTest, the first test class that package has had.
  • Tests-fix: PASS — verified empirically, not by reasoning. With the two production files stashed: writeJson_WhenParentDirectoryIsMissing_ShouldCreateIt failed on FileNotFoundException, testQuittingAtNamePromptCancelsCreation and testEndOfInputCancelsCreation failed on the assertion, and testQuittingAtScorePromptCancelsCreation never terminated at all (the pre-fix loop re-prompts forever when every answer is a quit token) — it was killed at 120s and completes in milliseconds once restored. With SystemConsoleInputProvider alone stashed, testReadLineReturnsNullWhenInputIsExhausted failed while its sibling passed.
  • Sibling structure: PASSSystemConsoleInputProviderTest mirrors the package of the class under test and follows the @SpringBootTest + plain-JUnit-assertion convention of ScoreCalculatorTest; the two new repo tests use the AssertJ style already established in that file.
  • Sibling renames: no signal this cycle — nothing was renamed.
  • Docs: PASSREADME.md records the cancel behaviour under Getting Started. CONTRIBUTING.md was re-read and its ./gradlew test instruction is still accurate. No Roadmap item is completed by this PR.
  • Issue resolution: PASSCreate directory for JSON storage if it doesn't already exist #32's named surface (writeJson's directory handling) and Allow user to quit early during project creation #37's (the interactive create prompts) are each changed, and neither issue is left partially resolved.
  • Manual validation: PASS on the signal-carrying jobs, one job assessed as carrying no signal. CI Pipeline / test, which runs ./gradlew test, is green on head 92353e3; locally the suite reports 96 tests, 0 failures, 0 skipped, up from 90 on main. Build and Test / build (21) is red, and is assessed as carrying no signal for this diff on three grounds: it fails in Validate Gradle wrapper on connect ETIMEDOUT 104.16.72.101:443, a network call made by a Node action before any Java step runs; the branch modifies no wrapper file, so the bytes being validated are identical to those in run 33367061037, where that same job passed on this same branch four minutes earlier; and the failure recurred identically on a retrigger, marking it as an outage rather than anything the diff could have caused. Because that job is the only one running ./gradlew build, that step was run locally rather than left unverified — BUILD SUCCESSFUL.
  • Shell command tests: PASSCreateProjectCommand is the only @ShellComponent touched and CreateProjectCommandTest gains three tests exercising its @ShellMethod.
  • Round-trip coverage: no signal this cycle — no field was added to Project or any other Lombok domain class.
  • I/O behind an interface: PASS — the new console handling stays inside the existing ConsoleInputProvider implementation and the new filesystem handling inside the existing ProjectJsonReaderWriter implementation; no command or service gained a direct dependency on java.io or System.in.

Findings raised and resolved during this review

Two problems were found in the first two commits and fixed in 4ab1ee1 rather than left for a reviewer:

  • src/main/java/com/preponderous/parpt/util/SystemConsoleInputProvider.java:12 — the README added by this PR promises that Ctrl-D cancels creation, but that only held when a real console was attached. The no-console fallback, which is the path taken by IDEs and by a piped stdin, calls Scanner.nextLine(), and that throws NoSuchElementException on an exhausted stream instead of returning null. Exhausted input would therefore have escaped as an unhandled exception, not the documented clean cancellation. EOF is now reported as null, matching Console.readLine().
  • src/main/java/com/preponderous/parpt/command/CreateProjectCommand.java:49readInput originally returned input.trim(), which quietly changed how every existing interactive answer was interpreted (a name entered with surrounding spaces would no longer round-trip as typed). That is outside what Allow user to quit early during project creation #37 asks for and was untested. Trimming is now confined to the quit-token comparison and the answer itself is returned untouched.

Judgment calls left visible for a human

  • The Enter 'q' at any prompt to cancel project creation. hint is printed from the command rather than woven into the prompt text, because the prompts live in application.yaml and are user-configurable, so they cannot be relied on to advertise the option. That hint is not asserted by any test, since nothing in this suite captures System.out.
  • CreationCancelledException is private static, unlike its neighbour InvalidScoreException, which is a public inner class. The difference is deliberate — the new type is an internal control signal and belongs on no public surface — but it is an inconsistency within one file and is flagged rather than hidden.
  • SystemConsoleInputProviderTest guards itself with assumeTrue(System.console() == null). On the JDK 21 toolchain this project pins, that holds and both tests run; a future JDK that hands back a Console for a redirected stream would skip them rather than fail misleadingly on a path System.setIn cannot drive.

This review was performed and posted during a Gardener session (https://github.com/Stephenson-Software/gardener).


drafted by Claude on behalf of Daniel Stephenson

@dmccoystephenson

Copy link
Copy Markdown
Member Author

Merge held for human review

This PR is complete and its gates are satisfied, but autonomous merge is withheld because one protected condition is matched.

Matched condition: a single file with more than 50 deleted lines. src/main/java/com/preponderous/parpt/command/CreateProjectCommand.java shows 66 deletions (git diff --numstat origin/main...HEAD). Those deletions are the five byte-identical score-retry loops being collapsed into promptForScore, plus the surrounding block being wrapped in the cancellation handler — no behaviour is dropped — but the threshold is a deliberate human-review trigger and is respected rather than argued past.

Everything else passed:

  • CI Pipeline / test is green on the head commit; the local suite reports 96 tests, 0 failures, 0 skipped, up from 90 on main.
  • Build and Test / build (21) is red in Validate Gradle wrapper on connect ETIMEDOUT 104.16.72.101:443, a network call made before any Java step runs. That job is assessed as carrying no signal for this diff: no wrapper file is modified by the branch, the same job passed on this same branch four minutes earlier over identical wrapper bytes, and the failure recurred on a retrigger. Since that job is the only one running ./gradlew build, that step was run locally instead of being left unverified — BUILD SUCCESSFUL.
  • Regression evidence was gathered empirically by stashing the production files and re-running the new tests, rather than argued from reading the diff. Details are in the self-review comment above.
  • The documentation sweep found README.md and CONTRIBUTING.md accurate against the implementation.

No other protected path is touched: .github/workflows/, build.gradle and src/main/resources/application.yaml are all unmodified.

Merging is left to a human review of that one file.

This comment was written during a Gardener session (https://github.com/Stephenson-Software/gardener).


drafted by Claude on behalf of Daniel Stephenson

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.

Allow user to quit early during project creation Create directory for JSON storage if it doesn't already exist

1 participant