fix(slack): decide the setup wizard's prompts by TTY, not by a flag - #483
Merged
Conversation
`jaw slack setup` built a readline interface whenever --non-interactive was absent, without ever asking whether there was a terminal to answer it. Under a pipe or </dev/null the first question() therefore waits for a line stdin will never produce: the promise does not settle, so it is not throwable and no try/catch can reach it. Node prints "unsettled top-level await" and exits 13, and the tokens it had already validated go unwritten (#475). Prompting is a property of the input, not of the flag, so `interactive` now requires both: no --non-interactive AND process.stdin.isTTY. The flag keeps its meaning for someone who wants defaults at a real terminal; the absence of a TTY decides the rest. The same guard covers the Step 1 conveniences, which is a user-visible fix on its own. Reading only the flag, a headless run still shelled out to `open` and threw a browser window at whoever ran it — noise for a host with nobody watching, and the reason a piped run could pop a window before hanging. Validation is untouched: a run with no bot token still exits 1 and names the flag to pass. This removes a crash, not a requirement. Fixes #475 Evidence: 8203 tests, 8176 pass, 1 pre-existing failure unrelated to Slack (project-git-summary, reproduced on unmodified origin/dev); tsc --noEmit clean; the three new non-TTY tests fail on origin/dev and pass here.
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: trueThanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What was wrong
jaw slack setupchose to prompt based on the--non-interactiveflag alone. Nothing in that decision looked at whether a terminal was attached, so a piped or redirected run still built a readline interface and awaited an answer that could never arrive.The failure is not an exception.
rl.question()waits for a line; a closed stdin never delivers one; the promise simply never settles. There is no error to catch, so the surroundingtryis powerless — node gives up on the event loop, printsDetected unsettled top-level await, and exits 13. Anything already validated is discarded, because the write happens after the prompts.Two distinct hangs, which is why "just pass
--channel-ids" was never the answer:< /dev/nullprintf '\n' |The fix
Prompting is a property of the input, not of the flag:
ask()already returned the default whenrlis null, so no other logic changed.--non-interactivekeeps its meaning for someone who wants defaults at a real terminal; the absence of a TTY decides the rest.This also stops an unwanted browser window. The Step 1 conveniences were behind
if (!nonInteractive), which reads the flag and not the input — so a headless run still shelled out toopenand launched a browser at whoever ran it, plus copied to the clipboard. Both are courtesies aimed at a person sitting at a terminal, so they now sit behind the sameinteractiveguard. A regression test asserts the binaries are never invoked, using recording stubs onPATHrather than trusting the absence of a log line.Validation is deliberately untouched. A run with no bot token still exits 1 and names the flag to pass. This removes a crash, not a requirement.
Why not the other two options in the issue
--non-interactive— already implemented; a no-op for this bug. It works, but only if you know to reach for it, and the failure gives you no hint that you should.--channel-ids— rejected. An emptychannelIdsis a valid default thatslackChannelScope()inscope-status.tsdocuments as "the shipped default and a normal way to run"; making it mandatory would contradict that contract. It also would not have fixed anything — the hang is not anchored to that prompt.Evidence
Both reproductions use
--skip-validate --no-notifyand a throwawayCLI_JAW_HOME, so no live Slack workspace or real settings file is involved.Before (at
origin/dev,28b8952):After:
Checks
tests/unit/slack-setup.test.ts. The three non-TTY ones fail onorigin/devand pass here (verified by stashing only the source change:pass 12 / fail 3→pass 15 / fail 0). They cover: a piped run completing and actually persisting tokens; noopen/pbcopyon a headless run; a missing token still exiting 1. The fourth pins the terminal path so this cannot regress into "never prompts".npm test— 8203 tests, 8176 pass, 1 fail. The single failure isproject git summary rejects repo roots outside the home guard, which fails identically on unmodifiedorigin/devand does not touch Slack.npx tsc --noEmit— clean.npm run build—dist/bin/commands/slack.js:146containsconst interactive = !nonInteractive && !!process.stdin.isTTY;.structure/verify-counts.sh—slack.tsupdated to 404L. One mismatch remains (skills_ref/registry.json, 3350→3324) which is pre-existing submodule drift onorigin/devand is left alone.Scope
#475 only. Deliberately not touched, both filed separately:
channelIdsis reported by health asmissing_channel_id.slacktomessaging.enabledChannels.A follow-up worth considering: setup writes settings only after every prompt, so a token that already passed
auth.testis thrown away if a later step fails. Moving the write earlier would make the wizard resumable, but it changes failure semantics and belongs in its own PR.Fixes #475