test(v0.18.2.fork.1): manifest edge cases — malformed jsonb + concurrent same-slug
Some checks failed
E2E Tests / Tier 1 (Mechanical) (push) Failing after 9s
E2E Tests / Tier 2 (LLM Skills) (push) Has been skipped

Closes Issue #9 from /plan-eng-review (user decision A: 加三個都).

Cache TTL hit/miss/invalidation already covered in
test/longest-prefix-match.test.ts. This file adds the two remaining
edge-case scenarios:

  - Malformed jsonb safe-skip: slug_prefix_rules = "not_an_array"
    string, mixed-type array entries, and 'null'::jsonb config all
    handled gracefully — bad rows skip, valid rows continue matching.
  - Concurrent put_page on same slug across two sources: both rows
    persist, composite UNIQUE (source_id, slug) does its job.

Note: manifest-jsonb-pglite.test.ts (originally planned in design
Phase 5 for engine parity) is dropped from scope. The implementation
parses jsonb in TypeScript via JSON.parse on the SELECT result,
not via SQL jsonb_array_elements / ->>operators, so PGLite vs
Postgres jsonb-operator parity is not exercised by manifest routing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-06 22:29:39 +08:00
parent 676d4283c7
commit 888fe26c24

View File

@@ -0,0 +1,127 @@
/**
* v0.18.2.fork.1 — manifest edge-cases (per /plan-eng-review Issue #9).
*
* Verifies the resolver gracefully handles cases that could only show up
* once real production data shape diverges from the happy path:
*
* - Malformed jsonb in sources.config (manually edited, partial corruption)
* → safe-skip the bad row, continue evaluating other sources
* - slug_prefix_rules: 'not_an_array' (string instead of string[])
* → safe-skip
* - slug_prefix_rules contains a non-string entry (mixed array)
* → skip the non-string entries, keep valid ones
* - Concurrent put_page on same slug across two distinct sources
* → both rows succeed (composite UNIQUE allows; no race on schema-level)
*/
import { describe, test, expect, beforeAll, afterAll, beforeEach } from 'bun:test';
import { PGLiteEngine } from '../src/core/pglite-engine.ts';
import {
resolveBySlugPrefix,
__invalidateSlugPrefixCache,
} from '../src/core/source-resolver.ts';
let engine: PGLiteEngine;
beforeAll(async () => {
engine = new PGLiteEngine();
await engine.connect({ type: 'pglite' } as never);
await engine.initSchema();
await engine.executeRaw(
`INSERT INTO sources (id, name, config) VALUES
('valid-source', 'valid-source', '{"federated": true, "slug_prefix_rules": ["valid/"]}'::jsonb),
('side-a', 'side-a', '{"federated": true}'::jsonb),
('side-b', 'side-b', '{"federated": true}'::jsonb)
ON CONFLICT (id) DO UPDATE SET config = EXCLUDED.config`,
);
});
afterAll(async () => {
await engine.disconnect();
});
beforeEach(() => {
__invalidateSlugPrefixCache();
});
describe('Malformed jsonb safe-skip', () => {
test('slug_prefix_rules = "not_an_array" (string) → resolver ignores that row, valid sources still match', async () => {
// Manually corrupt one source's config without going through CLI.
await engine.executeRaw(
`INSERT INTO sources (id, name, config) VALUES
('bad-string', 'bad-string', '{"slug_prefix_rules": "not_an_array"}'::jsonb)
ON CONFLICT (id) DO UPDATE SET config = EXCLUDED.config`,
);
__invalidateSlugPrefixCache();
// Valid source should still match its own prefix.
const r1 = await resolveBySlugPrefix(engine, 'valid/page');
expect(r1).toBe('valid-source');
// Bad source claims nothing — no slug routes there.
const r2 = await resolveBySlugPrefix(engine, 'not-an-array/x');
expect(r2).toBeNull();
});
test('slug_prefix_rules contains mixed-type entries → string entries kept, non-strings skipped', async () => {
await engine.executeRaw(
`INSERT INTO sources (id, name, config) VALUES
('mixed-types', 'mixed-types',
'{"slug_prefix_rules": ["good-prefix/", 42, null, "another-good/"]}'::jsonb)
ON CONFLICT (id) DO UPDATE SET config = EXCLUDED.config`,
);
__invalidateSlugPrefixCache();
const r1 = await resolveBySlugPrefix(engine, 'good-prefix/x');
expect(r1).toBe('mixed-types');
const r2 = await resolveBySlugPrefix(engine, 'another-good/y');
expect(r2).toBe('mixed-types');
});
test('config = null jsonb → safe-skip (NOT NULL constraint prevents in practice, but defensive)', async () => {
// PGLite's NOT NULL on sources.config will reject the literal NULL,
// so we test the edge by writing 'null' (jsonb null literal) which
// is allowed.
await engine.executeRaw(
`INSERT INTO sources (id, name, config) VALUES
('json-null', 'json-null', 'null'::jsonb)
ON CONFLICT (id) DO UPDATE SET config = EXCLUDED.config`,
);
__invalidateSlugPrefixCache();
// Resolver should skip cleanly — no slug routes to json-null.
const r = await resolveBySlugPrefix(engine, 'anything/x');
expect(r).toBeNull();
// And the valid source still works.
const r2 = await resolveBySlugPrefix(engine, 'valid/page');
expect(r2).toBe('valid-source');
});
});
describe('Concurrent put_page same slug across sources', () => {
test('same slug written to two different sources → both rows persist (composite UNIQUE)', async () => {
// Run both writes "concurrently" via Promise.all. PGLite is
// single-process so they serialize at the engine layer, but the SQL
// semantics still validate: composite UNIQUE on (source_id, slug)
// means both INSERTs fit without conflicting.
await Promise.all([
engine.putPage('shared-slug', {
type: 'note',
title: 'Side A',
compiled_truth: 'A side',
source_id: 'side-a',
}),
engine.putPage('shared-slug', {
type: 'note',
title: 'Side B',
compiled_truth: 'B side',
source_id: 'side-b',
}),
]);
const rows = await engine.executeRaw<{ source_id: string; title: string }>(
`SELECT source_id, title FROM pages WHERE slug = 'shared-slug' ORDER BY source_id`,
);
expect(rows.length).toBe(2);
expect(rows[0].source_id).toBe('side-a');
expect(rows[0].title).toBe('Side A');
expect(rows[1].source_id).toBe('side-b');
expect(rows[1].title).toBe('Side B');
});
});