feat(brief): tolerate a 404 on GET /brief with a one-shot reset (RB-22)
BriefStore.load() now treats a 404 from GET /brief as "no brief exists yet" and calls the existing reset() command once, instead of showing the generic load-failed error. BriefAdapter.load() gains a BriefLoadFailure error channel (notFound | error) so the store can tell a 404 apart from every other failure; every other adapter method stays on runSubmit, unchanged. The once-only bound is a field on the store, not a comment: a second 404 (from a later load() call) always falls through to the ordinary error path, and the recovery path never calls load() again, so no loop can form. This is the expand half of CQ-007's split (04-cqrs-light.md). Today's backend never 404s GET /brief, so the new branch is dead code until RB-23 (the backend contract half) ships in a later merge. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,12 @@ import { describe, it, expect, vi, afterEach } from 'vitest';
|
||||
import { Result } from '@shared/kernel/fp';
|
||||
import { Brief, BriefDecisions, CaseContext, LetterBlock } from '@brief/domain/brief';
|
||||
import { OrgTemplate } from '@brief/domain/org-template';
|
||||
import { BriefAdapter, BriefView } from '@brief/infrastructure/brief.adapter';
|
||||
import {
|
||||
BRIEF_LOAD_FAILED,
|
||||
BriefAdapter,
|
||||
BriefLoadFailure,
|
||||
BriefView,
|
||||
} from '@brief/infrastructure/brief.adapter';
|
||||
import { LetterPreviewAdapter, PREVIEW_FAILED } from '@brief/infrastructure/letter-preview.adapter';
|
||||
import { RevealBigNummerAdapter } from '@brief/infrastructure/reveal-bignummer.adapter';
|
||||
import { BriefStore } from './brief.store';
|
||||
@@ -60,7 +65,8 @@ describe('BriefStore action state (Idle | Busy | Failed)', () => {
|
||||
brief: { ...brief, status: { tag: 'approved', approvedBy: 'u2', approvedAt: 't' } },
|
||||
};
|
||||
const store = setup({
|
||||
load: (): Promise<Result<string, BriefView>> => Promise.resolve({ ok: true, value: view }),
|
||||
load: (): Promise<Result<BriefLoadFailure, BriefView>> =>
|
||||
Promise.resolve({ ok: true, value: view }),
|
||||
save: (): Promise<Result<string, BriefView>> => Promise.resolve({ ok: true, value: view }),
|
||||
approve: (): Promise<Result<string, BriefView>> =>
|
||||
Promise.resolve({ ok: true, value: approved }),
|
||||
@@ -79,7 +85,8 @@ describe('BriefStore action state (Idle | Busy | Failed)', () => {
|
||||
brief: { ...brief, status: { tag: 'approved', approvedBy: 'u2', approvedAt: 't' } },
|
||||
};
|
||||
const store = setup({
|
||||
load: (): Promise<Result<string, BriefView>> => Promise.resolve({ ok: true, value: view }),
|
||||
load: (): Promise<Result<BriefLoadFailure, BriefView>> =>
|
||||
Promise.resolve({ ok: true, value: view }),
|
||||
save: (): Promise<Result<string, BriefView>> => Promise.resolve({ ok: true, value: view }),
|
||||
approve: (): Promise<Result<string, BriefView>> =>
|
||||
Promise.resolve({ ok: true, value: approved }),
|
||||
@@ -93,7 +100,8 @@ describe('BriefStore action state (Idle | Busy | Failed)', () => {
|
||||
|
||||
it('goes Busy then Failed on a failing transition, surfacing the error', async () => {
|
||||
const store = setup({
|
||||
load: (): Promise<Result<string, BriefView>> => Promise.resolve({ ok: true, value: view }),
|
||||
load: (): Promise<Result<BriefLoadFailure, BriefView>> =>
|
||||
Promise.resolve({ ok: true, value: view }),
|
||||
save: (): Promise<Result<string, BriefView>> => Promise.resolve({ ok: true, value: view }),
|
||||
approve: (): Promise<Result<string, BriefView>> =>
|
||||
Promise.resolve({ ok: false, error: 'niet toegestaan' }),
|
||||
@@ -108,7 +116,8 @@ describe('BriefStore action state (Idle | Busy | Failed)', () => {
|
||||
it('a subsequent successful transition clears a prior Failed state', async () => {
|
||||
let approveResult: Result<string, BriefView> = { ok: false, error: 'eerste poging mislukt' };
|
||||
const store = setup({
|
||||
load: (): Promise<Result<string, BriefView>> => Promise.resolve({ ok: true, value: view }),
|
||||
load: (): Promise<Result<BriefLoadFailure, BriefView>> =>
|
||||
Promise.resolve({ ok: true, value: view }),
|
||||
save: (): Promise<Result<string, BriefView>> => Promise.resolve({ ok: true, value: view }),
|
||||
approve: (): Promise<Result<string, BriefView>> => Promise.resolve(approveResult),
|
||||
});
|
||||
@@ -156,8 +165,10 @@ function loadedBrief(store: BriefStore): Brief {
|
||||
}
|
||||
|
||||
async function loadedStore(over: Partial<BriefAdapter> = {}): Promise<BriefStore> {
|
||||
const ok = (v: BriefView): Promise<Result<string, BriefView>> =>
|
||||
Promise.resolve({ ok: true, value: v });
|
||||
// Untyped return (inferred as the narrow `{ ok: true; value }` literal) so this one
|
||||
// helper satisfies both `load` (error channel `BriefLoadFailure`) and `save` (error
|
||||
// channel `string`) — it only ever produces the `ok: true` branch.
|
||||
const ok = (v: BriefView) => Promise.resolve({ ok: true, value: v } as const);
|
||||
const store = setup({ load: () => ok(filledView), save: () => ok(filledView), ...over });
|
||||
await store.load();
|
||||
return store;
|
||||
@@ -255,8 +266,7 @@ describe('BriefStore rejection diff', () => {
|
||||
...filledBrief,
|
||||
status: { tag: 'rejected', rejectedBy: 'u2', rejectedAt: 't', comments: 'nee' },
|
||||
};
|
||||
const ok = (v: BriefView): Promise<Result<string, BriefView>> =>
|
||||
Promise.resolve({ ok: true, value: v });
|
||||
const ok = (v: BriefView) => Promise.resolve({ ok: true, value: v } as const);
|
||||
const store = setup({
|
||||
load: () => ok({ ...filledView, brief: submitted }),
|
||||
save: () => ok(filledView),
|
||||
@@ -283,7 +293,8 @@ describe('BriefStore.previewLetter', () => {
|
||||
|
||||
it('opens the composed letter in a new tab on success', async () => {
|
||||
const store = setup({
|
||||
load: (): Promise<Result<string, BriefView>> => Promise.resolve({ ok: true, value: view }),
|
||||
load: (): Promise<Result<BriefLoadFailure, BriefView>> =>
|
||||
Promise.resolve({ ok: true, value: view }),
|
||||
});
|
||||
await store.load();
|
||||
const blob = new Blob(['<html></html>'], { type: 'text/html' });
|
||||
@@ -301,7 +312,8 @@ describe('BriefStore.previewLetter', () => {
|
||||
|
||||
it('surfaces the error without opening a tab on failure', async () => {
|
||||
const store = setup({
|
||||
load: (): Promise<Result<string, BriefView>> => Promise.resolve({ ok: true, value: view }),
|
||||
load: (): Promise<Result<BriefLoadFailure, BriefView>> =>
|
||||
Promise.resolve({ ok: true, value: view }),
|
||||
});
|
||||
await store.load();
|
||||
const open = vi.spyOn(window, 'open').mockImplementation(() => null);
|
||||
@@ -377,3 +389,42 @@ describe('BriefStore.flushPending (CanDeactivate guard / beforeunload)', () => {
|
||||
expect(save).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
// --- RB-22 (CQ-007 expand half): a 404 from GET /brief tolerates by calling the
|
||||
// existing reset() command, exactly once. Today's backend never 404s (RB-23 adds
|
||||
// that); this fake adapter is what exercises the branch until then. ---
|
||||
|
||||
describe('BriefStore.load — 404 tolerance (RB-22)', () => {
|
||||
const notFound: Result<BriefLoadFailure, BriefView> = { ok: false, error: { tag: 'notFound' } };
|
||||
const resetOk: Result<string, BriefView> = { ok: true, value: view };
|
||||
|
||||
it('a 404 drives exactly one reset(), which populates the store', async () => {
|
||||
// Given GET /brief 404s (no brief exists yet) and reset() succeeds.
|
||||
const load = vi.fn(() => Promise.resolve(notFound));
|
||||
const reset = vi.fn(() => Promise.resolve(resetOk));
|
||||
const store = setup({ load, reset });
|
||||
|
||||
// When the store loads...
|
||||
await store.load();
|
||||
|
||||
// Then reset() ran exactly once, and the store ends up loaded from its result.
|
||||
expect(reset).toHaveBeenCalledTimes(1);
|
||||
expect(store.model().tag).toBe('loaded');
|
||||
});
|
||||
|
||||
it('a second 404 does not drive a second reset()', async () => {
|
||||
// Given every load() attempt 404s (e.g. the brief still fails to appear).
|
||||
const load = vi.fn(() => Promise.resolve(notFound));
|
||||
const reset = vi.fn(() => Promise.resolve(resetOk));
|
||||
const store = setup({ load, reset });
|
||||
|
||||
// When the store loads twice...
|
||||
await store.load();
|
||||
await store.load();
|
||||
|
||||
// Then reset() ran exactly once — the once-only bound holds across calls, not
|
||||
// just within one — and the second 404 surfaces as an ordinary load failure.
|
||||
expect(reset).toHaveBeenCalledTimes(1);
|
||||
expect(store.model()).toEqual({ tag: 'failed', reason: BRIEF_LOAD_FAILED });
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user