feat: Major refactor adding tests and addressing found issues - #608
Merged
Conversation
- logout now deletes the per-server token key it stores tokens under, along with the legacy un-namespaced token and the current workspace. - getConfigStore() is memoized so reads no longer construct three Configstores and re-run the legacy migration on every call. - Remove unused get-current-workspace-id.ts. - Move the INSIDE_WEB_BROWSER read into env.ts so every environment variable is read in one place. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KfGRqRqcApmECbDUkFuKfS
Pure moves with import updates only, no logic changes: - util/cli-args.ts -> args/parse.ts (CLI argument definition + parsing) - interact-for-*.ts -> interact/ (interactive prompting UX) - render-help.ts -> render/help.ts, completion/ -> render/completion/ (presentation renderers over the command spec) - validate-token.ts -> auth/ - util/read-stdin-json.ts -> output/ (stdin/stdout/stderr layer) - get-seam.ts -> seam/client.ts, util/request-seam-api.ts -> seam/request.ts (SDK init + request layer) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KfGRqRqcApmECbDUkFuKfS
- blueprint/source-npm.ts: fetch @seamapi/types from the npm registry, extract the OpenAPI module, and build a blueprint from it. - blueprint/cache.ts: the on-disk blueprint cache with TTL and version invalidation, atomic writes, and blueprint-version discovery. - blueprint/source-remote.ts: build a blueprint from the OpenAPI document served by the configured server. - blueprint/index.ts: getApiBlueprint source selector (was get-api-blueprint.ts). - blueprint/endpoint.ts: command path -> endpoint lookup and response key (was get-command-blueprint-def.ts + get-response-key.ts). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KfGRqRqcApmECbDUkFuKfS
Types and helpers live with something real, not in grab-bag modules: - util/prompt.ts -> interact/prompt.ts: the prompting primitive is the foundation of the interaction layer. - util/with-loading.ts -> output/with-loading.ts: a stderr spinner gated on the output format is an output concern. - util/ellipsis.ts -> render/text.ts: text truncation for display belongs to the presentation layer (markdown helpers join it later). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KfGRqRqcApmECbDUkFuKfS
Add context.ts with resolveAuth(): the server, token, and workspace are resolved once with env-over-config precedence and tagged with their source, replacing the per-value re-implementations in get-server.ts and get-credentials.ts (both deleted, tests ported to context.test.ts as a precedence table). CliContext replaces ContextHelpers (types.ts deleted) and now carries the config store and resolved auth alongside the blueprint and interactivity. seam/client.ts takes an AuthContext instead of re-resolving internally; blueprint/endpoint.ts narrows its dependency to just the blueprint. Config-mutating command bodies re-resolve after writes to keep today's ordering semantics (login --server stores the token under the new server's key). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KfGRqRqcApmECbDUkFuKfS
auth/operations.ts owns every auth/settings write: login, storeToken, logout, selectServer, selectWorkspace, selectFakeServer, and setUseRemoteApiDefs. The env-override policy now has one implementation, assertMutable, driven by the AuthContext source tags — the guards previously duplicated between the dispatcher and the interact modules are gone. The interact modules keep only prompting plus a call into operations; the dispatcher branches shrink to selection and messaging. login() stores the server before deriving the token key, covered by a unit test on that ordering. Behavior note: `config set fake-server` now clears the stored workspace selection, matching `select server` — a workspace from the previous server is not valid on the new one. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KfGRqRqcApmECbDUkFuKfS
Adds TESTING.md and conforms the tests to it: ConfigStore interface + createMemoryConfigStore + setConfigStore slot; PromptClient set/reset slot + createMemoryPrompt; injected validate dependency on login(). The three tests using vi.mock module-path fakes are rewritten against injected fakes; no vi.mock substitution remains. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KfGRqRqcApmECbDUkFuKfS
"Seam" names the whole product, not a layer. This layer is the SDK/HTTP edge, so it takes the SDK package's name (@seamapi/http): http/client.ts constructs SeamHttp, http/request.ts makes the request. TESTING.md references updated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KfGRqRqcApmECbDUkFuKfS
Command definitions previously lived in three places that had to agree by hand: the localCommands array (help and completion), the if/else dispatch chain in bin/cli.ts (execution), and a hardcoded list in the interactive picker. They had already drifted: config set fake-server existed only in the executor, wizard only in the spec. commands/registry.ts is now the single source of truth. A Command declares its definition, whether it needs a login, whether it is hidden, and how to execute, so the spec, the picker, and the dispatcher cannot disagree. Blueprint endpoints run through the generic commands/api-command.ts executor, which also owns the per-endpoint parameter policy and post-response follow-ups. bin/cli.ts shrinks to parsing, the login gate, and a dispatch loop whose 'back' navigation replaces the old self-recursion. assertKnownArgs moves to args/validate.ts; toPlainText/firstSentence move to render/text.ts; CliContext carries the output. Visible changes: the interactive picker now offers wizard (it was in help but missing from the picker), and config set fake-server loads the cached API definitions like every other dispatched command. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KfGRqRqcApmECbDUkFuKfS
The only place required params were checked was inside the interactive parameter editor, which is why validation lived in a UX module. args/validate.ts now owns assertRequiredParams; the api-command executor validates and sends directly on non-interactive runs, and the editor delegates to the same function for its nested-object flows. Error strings are unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KfGRqRqcApmECbDUkFuKfS
Argument values previously arrived typed by minimist's guessing: --is-managed true reached the API as the string "true" while the same parameter entered interactively was a real boolean, opaque strings were mangled into numbers unless hand-listed, and one list parameter was comma-split by a hardcoded hack. The api-command executor now re-reads argv with the endpoint's own parameter types (string-listing everything that is not a number or boolean) and args/coerce.ts turns each value into the JSON type its parameter documents: real booleans and numbers, comma-split lists typed per item, JSON-parsed objects, enum membership checked. A value that does not fit fails with a UsageError naming what the parameter expects instead of being sent for the API to reject. BREAKING CHANGE: request bodies for arguments change type. Booleans and lists that previously arrived as strings are now JSON booleans and arrays; values outside a documented enum are rejected client-side. Params piped in over stdin are passed through as given and are no longer comma-split. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KfGRqRqcApmECbDUkFuKfS
http/api.ts declares SeamApi — post params to a path, read back status and data — with createSeamApi() as the only place SeamHttp appears for raw requests, and createMemorySeamApi() as its in-memory test fake: a routes table plus a request capture, the in-process mirror of the e2e suite's HTTP server. requestSeamApi now takes its api and output as arguments, so the error-status-to-exit-code behavior is covered by a classical test with zero HTTP. Post-response follow-ups (connect webview open, action-attempt poll) move to http/follow-ups.ts. CliContext gains a lazy, per-run SeamApi accessor. UsageError and NonInteractiveError move to errors.ts alongside reportErrorAndExit, the top-level error-to-exit mapping formerly inlined in the entry's catch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KfGRqRqcApmECbDUkFuKfS
Fixtures live only in test/fixtures, and a test that consumes one is not a unit test: the blueprint-driven suites (commands/spec, commands/registry, render/help, render/completion) move under test/, mirroring the source layout, along with every test that reaches into another module for its fakes (context, auth/operations, http/request, interact-for-blueprint-object). A test now sits beside its module in src only when it tests the module of the same name and imports nothing beyond it, external packages and type-only imports excepted. TESTING.md records the rule. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KfGRqRqcApmECbDUkFuKfS
Where an interface has more than one implementation — the real edge and its memory fake — each implementation is now a class, matching the existing SeamConfigStore: SeamHttpApi and MemorySeamApi behind SeamApi, TerminalPromptClient and MemoryPromptClient behind PromptClient, MemoryConfigStore behind ConfigStore, and StreamOutput as the one stream-parameterized Output. The createFoo factories remain as the convenient constructors; call sites are unchanged apart from the memory fakes now being the capture themselves (api.requests, memoryPrompt.questions). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KfGRqRqcApmECbDUkFuKfS
Awaited expressions no longer hide inside call arguments, casts, or
object literals — each await lands on its own line in a const, so a
rejection's stack trace points at a named step instead of an
expression soup.
getApiBlueprint(false, { update }) told a reader nothing about what
false meant; the selector now takes a single options object,
getApiBlueprint({ useRemoteDefinitions, update }). selectFakeServer
likewise takes { urlSeed, config } instead of a positional seed that
call sites passed as undefined.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KfGRqRqcApmECbDUkFuKfS
Parent-relative imports are now an error: the configured import/no-relative-parent-imports rule was silently inert in this flat config, so the core no-restricted-imports rule enforces it instead. Every ../ import becomes a path alias — lib/* for source and a new test/* alias for fixtures — resolved by tsconfig paths, vitest, and tsc-alias in the build. Seam is the default context, so it earns no place in names: SeamConfigStore is really the PersistentConfigStore. The SeamApi port keeps its name as the one thing genuinely named after the API, with HttpSeamApi and MemorySeamApi as its implementations. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KfGRqRqcApmECbDUkFuKfS
interact/interact-for-device.ts said interact three times before the noun; the directory is now interactions/ and each file is just the noun: interactions/device.ts, interactions/command-selection.ts, and so on. The interactForFoo function names stay — they read as verbs at the call site. Likewise the create- prefix comes off module names: memory-output.ts, memory-config-store.ts, memory-prompt.ts, and memory-seam-api.ts each export their interface or class and its createFoo factory, and create-output.ts is simply output.ts, home of the Output interface and StreamOutput. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KfGRqRqcApmECbDUkFuKfS
interactions/ now holds nothing but the interactFor functions, re-exported through index.ts, so consumers import from lib/interactions/index.js without knowing the file-per-interaction layout. The prompt framework powering them — PromptClient, the terminal and memory clients, and the set/reset slot — moves out to sit beside the directory as lib/prompt.ts and lib/memory-prompt.ts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KfGRqRqcApmECbDUkFuKfS
Adds eslint-import-resolver-typescript plus the import/parsers setting so eslint-plugin-import can resolve .js-suffixed TypeScript imports and parse the imported files — without both, its graph-based rules see nothing and pass silently. import/no-cycle is now on (verified against a deliberate cycle) and immediately caught a real one: config-store and migrate imported each other, broken by extracting the shared pure transforms into config/values.ts. Two rules stay off, documented in place: import/extensions demands .ts extensions once the resolver maps imports to their .ts files (nodenext already fails the build on a bad extension), and import/no-relative-parent-imports turns out to ban depending on parent directories however the import is spelled — alias imports included — which is not the specifier rule this repo wants; the core no-restricted-imports pattern keeps doing that job. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KfGRqRqcApmECbDUkFuKfS
The port no longer imitates a transport with its own post method — it prepares requests the way the SDK itself models them. SeamApi.createRequest returns a SeamApiRequest (url, method, body, fetchResponse), and the real implementation hands back an actual SeamHttpRequest, so the reported request banner now shows the full resolved URL and error statuses arrive as the SDK's typed SeamHttpApiError instead of hand-rolled status checks. The memory fake rejects with those same SDK error classes, never an imitation. requestSeamApi returns the response body (null on an API error) and renders the error payload from the typed error: type, message, and data. Visible changes: the banner prints the full request URL rather than the bare path, the informational [200] status line on success is gone, and a non-Seam-shaped error response (e.g. proxy HTML) now reports as a CLI error instead of being printed as a payload. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KfGRqRqcApmECbDUkFuKfS
razor-x
force-pushed
the
claude/code-arch-srp-layers-gcnld8
branch
from
August 5, 2026 16:50
e31c60d to
9ae1881
Compare
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.
along with the legacy un-namespaced token and the current workspace.
Configstores and re-run the legacy migration on every call.
variable is read in one place.
Co-Authored-By: Claude Fable 5 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_01KfGRqRqcApmECbDUkFuKfS