fix: restore vf docs search, and make the test suite hermetic and green - #37
Bradenream wants to merge 12 commits into
Conversation
The hermetic suite spawned vf with the developer's own HOME, so it read their ~/.config/vf and, on macOS, their login Keychain. Tests that expect no credentials (the no-token preflight cases) found a session and failed, and a run refreshed the real OAuth session: it rewrote oauth.json and rotated the tokens in the Keychain. test/setup.ts now points HOME at a fresh temp directory for each test file and removes it afterwards. Under an empty HOME, /usr/bin/security has no default keychain either, so vf can neither read nor overwrite the developer's tokens. The integration tests still authenticate with VF_TOKEN from .env.test. Checked against a decoy HOME holding a canary token and an OAuth session: before, 5 no-token tests read the canary and vf wrote oauth.lock into it; after, every one passes and the decoy is byte-for-byte unchanged.
vf docs search called search_voiceflow_documentation, a tool the docs MCP
server no longer has. The server now offers search_voiceflow, which takes
a question plus who is asking and why, and returns its hits as
structuredContent instead of Title:/Link:/Page:/Content: text blocks.
The server reports an unknown tool inside a successful JSON-RPC result
(isError: true), and vf never read that flag. It printed "no such tool"
as if it were a search hit and exited 0, and --output-format json
returned a single hit with every field empty. That is what the two red
docs-command tests were catching.
vf now calls search_voiceflow and maps each hit to the same
{title, link, page, content} it printed before, with page cut from the
hit's page URL so it can go straight to vf docs get. A tool error, or a
response without structured results, is now an error.
The help check named its flags by hand: agent update's --instructions and --prompt, whose descriptions had backticked words, and transcript search's --version-param. The latest regeneration gave --instructions and --prompt the description "string value". Their two checks kept passing with nothing left to check, and the prose check, which looked for "Backticked 'Name' resolves to", failed. The flags now come from vf --usage, which keeps each description as the spec wrote it. Every flag whose description has a backtick is checked in its command's --help: its description must appear with the backticks turned into quotes. Had pflag lifted a word as the value placeholder, that word would appear bare and the description would not match. A separate case fails if no such flag is found, so the check cannot pass empty. With the neutralization removed from flagDescription, this now reports all 16 flags it affects today: --playbooks renders as "--playbooks workflows", --label as "--label name", --exclude as "--exclude *".
"Leaves Markup-valued flags strict" passed --url 'not json' to mcp-server create and expected a rejection, because --url was []components.Markup. The latest regeneration made that field a plain string and removed Markup from the spec, so the text was accepted and the test went red. The behaviour test now uses transcript search's --filters, a []components.TranscriptFilterUnion: a list of unions, the shape --url had. A list is not a string, so raw text is still an error there. That check cannot see the sharper case the review asked about: a union with a string member, outside a list, which would decode quoted text if the raw-text fallback ever ran on it. A reflection probe over all 90 JSON flags found none with that shape today. So targetsStringValue gets a unit test with its own types: string, *string, a string enum and an optional-nullable string take raw text; a union with a string member, a list, a map and an optional-nullable union do not. Loosening the rule to accept those fails four of its cases.
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
HOME isolation does not prevent access to real OS keyrings on all supported platforms.
Review effort: Balanced
Findings: 1
Open (1)
What changed in this PR
Restores documentation search and improves behavior-suite isolation and reliability.
Changes:
- Updates docs search for the new MCP contract.
- Isolates tests from the developer’s home directory.
- Refreshes flag behavior tests and adds focused Go coverage.
| File | Description |
|---|---|
test/setup.ts |
Creates a temporary test HOME. |
test/flag-raw-text.test.ts |
Updates strict union-flag coverage. |
test/flag-errors.test.ts |
Dynamically validates backticked flag descriptions. |
internal/flagutil/stringvalue_test.go |
Tests string-target type detection. |
internal/cli/docs.go |
Implements the new docs search contract. |
internal/cli/docs_test.go |
Tests response parsing, arguments, and page paths. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
An empty HOME isolates ~/.config/vf everywhere, and the login Keychain on macOS, where /usr/bin/security finds it through $HOME. On Linux, vf's keyring (zalando/go-keyring) reaches the Secret Service over the D-Bus session bus, and godbus finds that bus through DBUS_SESSION_BUS_ADDRESS or $XDG_RUNTIME_DIR/bus, never $HOME. So a developer's token in GNOME Keyring or KWallet would still have been read by the no-token tests, and an auth flow could have overwritten it. test/setup.ts now also points DBUS_SESSION_BUS_ADDRESS at a socket that does not exist. godbus uses an address that is set rather than its fallback, and fails at once. go-keyring returns that error, and vf then treats the keyring as unavailable, so it neither reads nor writes it. Checked against the versions vf builds with: go-keyring v0.2.6 and godbus v5.1.0.
The no-token cases ran vf with an empty HOME, which hides the macOS Keychain, but on Linux vf reaches the keyring over the D-Bus session bus. A developer with a token saved there would have had it found, and the cases would have taken a different path. They now point DBUS_SESSION_BUS_ADDRESS at a socket that does not exist, as test/setup.ts does for the whole suite in #37. Copilot raised this in review.
z4o4z
left a comment
There was a problem hiding this comment.
Left 10 inline comments. The ones I'd fix before merging:
flag-raw-text.test.ts:97. The--filtersstrictness test passes whether or not the raw-text fallback runs on that flag.setup.ts:7.VF_*variables exported in the developer's shell still override.env.test, so the suite isn't fully hermetic and could use a real token.docs.go:311. Combined with the existing first-data:-line SSE parsing, the new "no structured results" error makes search fail if the server sends a notification before the result.
The rest are edge cases or test-robustness notes. The no-results prose and --jq comments are about behaviour that predates this PR.
What I checked: the new Go tests pass. I simulated the new help-text check against a locally built vf: it finds all 16 flags with no problems. I couldn't run the live docs calls or the vitest suite in my sandbox.
Generated by Claude Code
The --filters strictness check asserted only "invalid value for --filters". Had the raw-text fallback run on that flag, the text would have been quoted into valid JSON that then failed to decode as a list, and the error starts with the same words, so the check could not fail. Review on #37 caught it. The check now asserts the strict path's own wording ("the value is not valid JSON") and the absence of the fallback's ("valid JSON but not the shape"). With targetsStringValue forced to run the fallback on every flag, it now fails.
dotenv never overrides a variable that is already set, so a VF_TOKEN, VF_WORKSPACE_ID or VF_OUTPUT_FORMAT exported in the developer's shell won over .env.test. With a real token and workspace in the shell, the config counted them as credentials and queued all 16 integration test files, which create and delete projects in that workspace. Every hermetic test's vf also saw the token. Review on #37 caught it. test/env.ts now drops every VF_ variable except VF_SKIP_INTEGRATION_TESTS, the switch CI sets on purpose, before loading .env.test. vitest.config.mts and test/setup.ts both use it. With shell credentials and no .env.test, the suite now stops with its usual "need VF_TOKEN and VF_WORKSPACE_ID" error instead of using them.
Isolation rests on how each platform finds its credential store: the macOS Keychain through $HOME, a Linux keyring over D-Bus. If a platform ever found its store another way, the suite would quietly run against the developer's account again. Review on #37 raised this. test/isolation.test.ts runs vf whoami the way every test runs vf, and requires no token from any source (flag, environment, keyring or config file), no OAuth session, and a config file outside the real home. With VF_TOKEN exported in the shell and the previous commit undone, it fails and shows the token's source as [env].
The docs server answers with server-sent events, and vf read only the first data: line. A stream that sends another message first, such as a progress notification, or that splits the reply across several data: lines, would have made every search fail with "no structured results". Review on #37 caught it. vf now joins each event's data: lines and takes the first message that carries a result or an error; notifications carry neither. A stream with no reply in it is still an error.
Two gaps in reading a search reply, found in review on #37: - A tool error with no text gave "documentation search failed: —", naming no cause. It now says the tool reported an error without saying why. - A hit without a pageUrl got an empty page, and the suggested "vf docs get <page>" then failed. The page now comes from the hit's link, with its #section dropped.
Two problems with vf docs search's structured output, raised in review on #37. Both predate this PR. - With no matches it printed a sentence of prose even under --output-format json, so a caller parsing stdout as JSON failed. It now prints []. - --jq switched the output to JSON but the expression was never applied: --jq '.[0].page' printed the whole list. It now runs through output.ApplyJqFilter, as other commands use it.
Three gaps in the help-text check, found in review on #37: - Each description was matched against the command's whole --help. A flag whose backticks were lifted could pass on another flag's identical text. Each flag is now checked against its own entry: its line and the lines its description wraps onto. - Nothing checked the --flag <type> label itself, though the describe title promises it. The label must now be one of pflag's type names, or absent for a boolean. With neutralization removed, all 16 flags report the lifted word, including --metadata's "{ key, values }". - The --help runs were awaited one after another inside one test, and the KDL schema was decoded with JSON.parse, which throws on a KDL-only escape such as \u{...}. The runs now go in parallel, and KDL escapes are decoded properly.
Merge activity
|

Summary
Two problems made the behaviour suite untrustworthy, and one of them hid a real outage.
1. The suite ran against the developer's own HOME. Every
vfit spawned read~/.config/vfand, on macOS, the login Keychain. The no-token tests found a real session and failed. Worse, a run refreshed the real OAuth session: it rewroteoauth.jsonand rotated the tokens in the Keychain.test/setup.tsnow gives each test file an empty temp HOME, and cuts off the OS keyring on both platforms the suite runs on:/usr/bin/securityhas no default keychain.DBUS_SESSION_BUS_ADDRESSat a socket that does not exist.Either way, vf can neither read nor overwrite the developer's tokens. The integration tests still authenticate with
VF_TOKENfrom.env.test.2. Four tests were red on master (
docs-command×2,flag-errors,flag-raw-text), for three different reasons:vf docs searchis broken for every user. The docs MCP server renamedsearch_voiceflow_documentationtosearch_voiceflow, which takesquestion,initiator,purposeandwhy, and returnsstructuredContent. The server reports an unknown tool inside a successful result (isError: true), and vf never checked that flag: it printed "no such tool" as a search hit and exited 0. vf now calls the new tool and keeps the same{title, link, page, content}output. A tool error is now an error.vf --usage(16 today) and checks each one's--help. A separate case fails if it finds none.mcp-server create --urla plain string and removedMarkupfrom the spec. The check now usestranscript search --filters, a list of unions: the shape--urlused to have. A reflection probe over all 90 JSON flags found none that is a union with a string member outside a list. So a unit test now pinstargetsStringValueon its own types, which the spec cannot change.Changes from review
VF_*settings from.env.testalone (test/env.ts).VF_SKIP_INTEGRATION_TESTSis the one exception. Before, aVF_TOKENandVF_WORKSPACE_IDexported in the developer's shell queued all 16 integration test files against that account.test/isolation.test.tsfails if a vf spawned by the suite finds a token from any source, an OAuth session, or a config file in the real home.vf docs search:pageUrlis missing;[]for no matches in JSON output, and applies--jq.--helpruns go in parallel, and KDL escapes are decoded properly.--filtersstrictness check: it now tells the raw-text fallback apart from a decode failure.Before and after
vf docs search "personal access token"Test plan
gofmt,go vet ./...andgo test ./...pass;go.modis unchanged.internal/cli/docs_test.gocovers parsing,isError, page paths and the tool's required arguments.internal/flagutil/stringvalue_test.gocovers the raw-text type rule. Loosening the rule fails 4 of its cases.6df4604). Together with fix: stop waiting on the silent stdin an agent's shell hands vf #38–fix: hide camelCase secrets in --dry-run and --debug output #42 it passes 103/103, with a shellVF_TOKENexported and the decoy HOME unchanged.vf docs searchin human, JSON and agent mode. Each hit'spageworks withvf docs get.The
docs-commandtests hit the live docs site, so they will catch the next contract change too.