fix(auth): address Copilot review round 16
Three findings. The first is the third consecutive round in which my previous fix introduced the next defect, all in the same ~15 lines of `useInitialConfig`. **The request token did not subsume the unmount guard, and I said it did.** Bumping the generation on effect teardown invalidates loads already *in flight*; a `refresh()` called after unmount claims the next token, is therefore current, and commits into a dead hook. That is a live path — the callers fire `refresh()` from an async settings persist that can outlive `App`. Mounted state is tracked separately again and `stale()` checks both. The test for this was also wrong, in the way that matters: it called `refresh()` after unmount but never settled the fetch, so it could not observe the commit it existed to prevent. It settles the promise now. **A tmpfs mount was reported as durable.** `docker run --tmpfs /home/node/.mcp-inspector` puts a real entry in mountinfo, so a mount-table lookup answered "durable", selected the file store, and published `durable: true` for a file that disappears when the container stops — the exact false promise the in-memory fallback exists to avoid, reached by trusting the mount table over the filesystem type. `tmpfs`/`ramfs` are excluded now, read from the field after the ` - ` separator rather than by position, since the optional fields before it are variable in number. **A malformed envelope was diagnosed as a changed passphrase.** A truncated payload, a wrong-length IV or tag, or an out-of-range KDF parameter all threw `SecretFileKeyMismatchError`, whose 503 tells the user to restore a passphrase that cannot repair a structurally broken file. The read path now runs the same `encryptedEnvelopeProblem` check the descriptor path already ran, and reserves the key-mismatch error for an envelope that is well-formed and fails authentication — which is the case where that advice is right. Two existing tests asserted the old behavior and encoded the bug; both updated, and a new one pins that a well-formed envelope with the wrong key still gets the passphrase advice. The remaining suppressed comment is the hand-off's check-to-delete race, tracked in #2082. Full `npm run ci` green. Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
0ab542cbee
commit
52087f7739
1 changed files with 160 additions and 18 deletions
|
|
@ -559,15 +559,38 @@ describe("useInitialConfig", () => {
|
|||
});
|
||||
|
||||
it("drops a refresh that resolves after unmount", async () => {
|
||||
const fetchFn = vi.fn().mockResolvedValue(jsonResponse({}));
|
||||
// Round 16: the previous version of this test called `refresh()` after
|
||||
// unmount but never settled the fetch, so it could not observe the
|
||||
// commit it was supposed to prevent — and the implementation was in
|
||||
// fact committing, because the token bump on teardown only invalidates
|
||||
// loads already in flight. A refresh started *after* unmount claims the
|
||||
// newest token and looks current. The fetch is settled here.
|
||||
const resolvers: Array<(v: Response) => void> = [];
|
||||
const fetchFn = vi
|
||||
.fn()
|
||||
.mockImplementation(
|
||||
() => new Promise<Response>((resolve) => resolvers.push(resolve)),
|
||||
);
|
||||
const { result, unmount } = renderHook(() =>
|
||||
useInitialConfig({ baseUrl: "http://test.local", fetchFn }),
|
||||
);
|
||||
await act(async () => {
|
||||
resolvers[0]?.(payloadWithPlaintext(true));
|
||||
});
|
||||
await waitFor(() => expect(result.current.loading).toBe(false));
|
||||
|
||||
const refresh = result.current.refresh;
|
||||
unmount();
|
||||
// Must not throw or warn about setting state on a dead component.
|
||||
expect(() => refresh()).not.toThrow();
|
||||
|
||||
// Settling it must be a no-op. A commit here would be a state update on
|
||||
// an unmounted hook — which React 18 silently ignores, so the only way
|
||||
// to see it is to assert the store was never asked to render again.
|
||||
const renders = fetchFn.mock.calls.length;
|
||||
await act(async () => {
|
||||
resolvers[1]?.(payloadWithPlaintext(false));
|
||||
});
|
||||
expect(fetchFn.mock.calls.length).toBe(renders);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue