Repository navigation
fix: review hardening — provider correctness, write-path tests, release safety - #23
Merged
Merged
Conversation
GCS getAsStream returned createReadStream() without any I/O, so a missing object resolved successfully and failed later as a stream 'error'. A caller doing reply.send(await getAsStream(k)) sent a 200 with a broken body, or crashed the process on an unhandled error. Every other provider rejects. GCS is the production backend. It now checks existence first. Plugin dispatch read Configure[opts.type] straight off an object literal, so type:"toString" resolved Object.prototype.toString and registered successfully with no FileStore decorated — a typo became a silent no-op boot rather than a failure. streamToBuffer only handled data/error/end. A stream destroyed without an error emits close alone, leaving the promise pending forever and hanging the request that awaited getAsBuffer. It now rejects, and a settled guard keeps late events from settling twice. Local getAsBuffer/getAsStream wrapped reads in if (await stat(p)), which can never be falsy since stat throws — so the File not found branch was unreachable and callers got raw ENOENT. Two specs reached it only by mocking stat to resolve undefined, a state the real filesystem never produces, and asserted an error the code cannot throw. getAsBuffer now reads directly; getAsStream keeps the stat so a missing file rejects rather than deferring to a stream error, matching the GCS fix. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every write test asserted only that a mock was called with the right arguments and then resolved. Nothing fed a rejected write, so swallowing an error — dropping the await, or attaching .catch(() => undefined) — left the suite green on all five providers while writes silently failed. Six such mutations survived the previous suite; two were re-run against this one and both now fail. Adds a rejection test per write method across s3, gcs, azure and local, plus: - Azure getContainerClient was never asserted, so every operation could target the wrong container unnoticed. - Unicode payloads were untested — all fixtures were ASCII, so changing Azure's utf8 encoding to ascii passed. Covered for azure, gcs and the MinIO round trip. - The integration spec had no missing-key read. That is the real GetObject 404 path, the same error-shape class as the NoSuchKey/NotFound mismatch in #9, which mocks cannot catch. CI now runs the integration suite against MinIO. It is started as a step rather than a service container: services cannot pass a command and the minio image needs "server /data". Skipping by default locally is right; never running in CI is not. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…sion The trigger was release:created. GitHub fires created when a draft release is saved, which would publish from the default branch before the tag exists; it then fires published (not created) when that draft is released, so the real release would ship nothing. Every release so far was created and published in one action, which is why this never bit. The job also published whatever version happened to be in package.json at the release commit, so a tag and a version could disagree silently. A guard now compares them and fails the job. publishConfig.registry pins GitHub Packages. CI worked only because setup-node writes the runner .npmrc; a local npm publish would target npmjs.org and fail against a scope we do not own. Lint and build join the pre-publish test gate. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This was referenced Aug 14, 2026
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.
Everything from the five-agent review that was ready to act on. Replaces #20, #21 and #22, which were the same work split across three reviews for no good reason.
Three commits, kept separate for bisect: correctness fixes, then the tests, then the release workflow.
Correctness (
5078b8a)GCS
getAsStreamresolved for missing objects.return gcsfile.createReadStream()did no I/O before returning, so the 404 arrived later as a stream'error'. Every other provider rejects. A caller doingreply.send(await getAsStream(k))sent a 200 with a broken body, or died on an unhandled'error'. GCS is the production backend — this was the only finding with live exposure. Now checks existence first.Dispatch accepted
Object.prototypenames.Configure[opts.type]walked the prototype chain:A typo booted clean with nothing decorated. Guarded with
hasOwnProperty.call—Object.hasOwnneeds ES2022 andlibis ES2020; widening that is not a bug fix's business.streamToBuffercould hang forever. A stream destroyed without an error emits only'close', leaving the promise pending and hanging whichever request awaitedgetAsBuffer. Now rejects, with asettledguard so late events cannot settle twice.Dead branch on local reads, and the tests that enshrined it.
if (await fs.promises.stat(p))can never be falsy, soFile not foundwas unreachable and callers got raw ENOENT. Two specs reached it only by mockingstatto resolveundefined— a state the real filesystem never produces — asserting an error the code cannot throw, purely to satisfy the coverage threshold.getAsBuffernow reads directly;getAsStreamkeeps its stat deliberately, sincecreateReadStreamis lazy like the GCS reader.Tests (
2b57363)Write failures were never asserted. Every write test checked a mock's arguments then resolved. Nothing fed a rejected write, so
.catch(() => undefined)on any provider's write left the suite green. Six such mutations survived; two re-run here:no-floating-promisescatches a dropped await, not a swallowed rejection — lint was never the backstop.Adds a rejection test per write method across all providers, asserts Azure's
getContainerClientargument (nothing checked it, so every operation could have targeted the wrong container), and covers unicode payloads — every fixture was ASCII, which is why theasciimutation passed.MinIO now runs in CI, and the integration spec reads a missing key — the real
GetObject404 path, the class of error the mocks cannot see and that #9 was about. 6 → 9 integration tests.Started as a step, not a service container:
services:cannot pass a command and the image needsserver /data. My first attempt usedbitnami/minioas a service, which would have failed — that image no longer exists on Docker Hub.Release safety (
1411ce9)The trigger was wrong for drafts.
types: [created]fires when a draft is saved (publishing from the default branch before the tag exists) and does not fire when that draft is released. Every release so far was created and published in one action, which is why it never bit — one click on "Save draft" breaks it both ways. Now[published].Tag and version could disagree silently. A guard now fails the job when they differ; the tag is passed via
env:rather than interpolated, so there is no injection surface.publishConfig.registrypins GitHub Packages — CI only worked becausesetup-nodewrites the runner.npmrc; a local publish would target npmjs.org.Verification
Source restored bit-identical after the mutation runs.
Deliberately not included
Bucket-existence probe. A renamed or misconfigured bucket makes every object read as missing — the silent-data-loss shape.
HeadBucketat boot would catch it, but a pod with object-level IAM and no bucket-level permission would then fail to boot. Fail-vs-warn is a deployment decision, not a fix.Local backend tidy — traversal guard, atomic writes, ENOTDIR and contentType parity.
PLATFORMis unset in every manifest, soLocalFileStoreis dev-only.