fix(plugins): accept weak catalog etags

This commit is contained in:
2026-09-01 12:17:31 +08:00
parent 5e8b7266c2
commit 38f2358db3
3 changed files with 88 additions and 1 deletions

View File

@@ -0,0 +1,59 @@
# Task: Diagnose MakeLore Marketplace catalog loading
## Identity
- Task ID: 20260901-plugin-catalog-load-client-7d4a8c21
- Mode: Feature
- Branch: codex/20260901-plugin-catalog-load-client-7d4a8c21-plugin-catalog-load-client
- Worktree: D:\Datas\OthersProjects\makelore-plugin-catalog-load-client-7d4a8c21
- Base commit: 5e8b7266c2a8297c9d307b576256886659d44780
- Owner: codex-root
- Status: Ready for Integration
## Scope
- Reproduce the installed MakeLore Marketplace catalog failure at the Renderer → IPC dispatcher → Main Marketplace client boundary.
- Verify the installed 1.2.0 artifact contains the current Marketplace composition and compare its parser/network behavior with the live catalog.
- If a client defect is confirmed, change only the smallest Marketplace client/Host API/error-projection seam and focused tests.
## Intent And Constraints
- Preserve the occupied client root worktree and its untracked packaging record; all product work stays in this isolated worktree.
- Do not expose Works credentials, read real `.env` files, alter Package Store/Provider/billing state, or publish a package.
- Treat local-package inference as forbidden: a failed server catalog must remain fail closed.
- The tight loop must reproduce the exact initial catalog failure or prove each boundary healthy before any implementation change.
## Plan
1. Verify the live catalog parses through the exact client DTO parser.
2. Inspect the running installed Main process, local Host API, logs, embedded bundle, and effective API origin.
3. Reproduce the failing boundary with a focused test/probe, rank falsifiable causes, and implement only the confirmed fix.
4. Run focused regressions and task documentation gates; do not rebuild/publish unless the fix affects packaged behavior and needs an artifact proof.
## Outcome
- Completed. The Marketplace client now accepts both strong and standards-compliant weak composite ETags, while preserving the exact received validator for the next `If-None-Match` request and retaining all generation/pricing identity checks.
- Root cause: the production gateway emits `W/"plugins-..."` for the compressed catalog response. The previous strong-only regular expression rejected that otherwise valid HTTP 200 response, and the local Host API surfaced the rejection as `plugin_backend_unavailable`.
- No fallback catalog, server mutation, proxy workaround, or billing/Provider behavior was added.
## Verification
- Live catalog parsed through `parseMarketplaceCatalogPage`: HTTP 200, total 3, exact IDs `makelore.data-service`, `makelore.game-resource`, `makelore.web-search`.
- Installed MakeLore is version 1.2.0 and its `app.asar` contains the Marketplace route, service composition, and production origin `https://square.nianxx.cn`.
- Running local Host API is on `127.0.0.1:13210`; an unauthenticated shell probe correctly returns 401 and therefore cannot yet distinguish dispatcher/service from upstream failure.
- Installed-session Electron probes returned HTTP 200 and the weak composite ETag; the exact source Marketplace client failed before the fix with `marketplace_response_invalid: invalid composite Marketplace ETag` and returned all three official Plugins after the fix.
- TDD regression: the new weak-ETag/304-reuse case failed before the product change and passed after it.
- `pnpm exec vitest run tests/unit/coding-plugin-marketplace-client.test.ts tests/unit/plugin-marketplace-routes.test.ts tests/unit/plugin-marketplace-store.test.ts tests/unit/plugin-marketplace-pages.test.tsx`: 4 files, 68 tests passed.
- `pnpm test`: 214 files, 1761 tests passed, 2 expected skips; pressure test 1/1 passed.
- `pnpm run typecheck`: passed.
- `pnpm run lint:check`: 0 errors and 5 unchanged warnings outside this task.
- `pnpm run build:vite`: Renderer, Main, Preload, and utility builds passed.
- Scoped ESLint and `git diff --check`: passed.
## Follow-ups
- Integrate this task into the client main branch, rebuild the Windows artifact, and install/restart it. The currently installed MakeLore 1.2.0 artifact cannot pick up this source-only fix dynamically.
## Promotion Candidates
- None recorded.

View File

@@ -44,7 +44,7 @@ const PLUGIN_ID_PATTERN = /^[a-z][a-z0-9.-]{0,127}$/u;
const SHA256_PATTERN = /^[a-f0-9]{64}$/u;
const RELEASE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u;
const REQUEST_ID_PATTERN = /^[\x21-\x7e]{1,128}$/u;
const ETAG_PATTERN = /^"plugins-(\d+)-tp-([A-Za-z0-9._-]{1,128})"$/u;
const ETAG_PATTERN = /^(?:W\/)?"plugins-(\d+)-tp-([A-Za-z0-9._-]{1,128})"$/u;
type UnknownRecord = Record<string, unknown>;
type FetchImplementation = (input: string | URL, init?: RequestInit) => Promise<Response>;

View File

@@ -350,6 +350,34 @@ describe('Marketplace client and account cache', () => {
await expect(client.readCatalog({ limit: 10 })).resolves.toMatchObject({ stale: true, total: 1 });
});
it('accepts a weak composite ETag from compressed Marketplace responses and reuses it verbatim', async () => {
const pricingVersionId = '00000000-0000-0000-0000-000000000420';
const etag = `W/"plugins-2-tp-${pricingVersionId}"`;
const fetcher = vi.fn<typeof fetch>()
.mockResolvedValueOnce(response({ ...catalogPage, catalog_generation: 2 }, {}, {
ETag: etag,
'X-Plugin-Catalog-Generation': '2',
'X-Token-Point-Pricing-Version': pricingVersionId,
}))
.mockResolvedValueOnce(new Response(null, { status: 304 }));
const client = createMarketplaceClient({
fetchImpl: fetcher,
apiBaseUrl: 'https://square.example',
getAccessToken: async () => null,
subscribeSession: () => () => undefined,
});
await expect(client.readCatalog({ limit: 10 })).resolves.toMatchObject({
total: 1,
etag,
generation: 2,
pricingVersionId,
stale: false,
});
await expect(client.readCatalog({ limit: 10 })).resolves.toMatchObject({ etag, stale: false });
expect(fetcher.mock.calls[1]?.[1]?.headers).toMatchObject({ 'If-None-Match': etag });
});
it('refreshes download authentication at most once before accepting the artifact', async () => {
const archive = Buffer.from('signed-artifact');
const { grant } = signedGrant(archive);