mirror of
https://github.com/wu736139669/hapi.git
synced 2026-10-18 20:59:49 +00:00
feat(fixtures): SSE patch + pagination scenario fixtures (K7)
Two new golden-fixture suites, expectations machine-generated from the web
implementation (same source-of-truth principle as the chat suite):
- shared/fixtures/sse/ (12 cases): session-updated versioned-patch
application via applySessionDetailPatch — strict version gates for
metadata/agentState/todos/teamState, out-of-order arrival, teamState:null
clear, max-monotonic updatedAt, flat last-write-wins fields, sub-minute
activeAt keep-alive drop, scratchlistUpdatedAt trigger, and the pinned
actual behavior that activeTurnStartedAt is NOT applied by the patch path.
Inputs are stored schema-normalized and validated against SessionSchema /
strict SessionPatchSchema at generation time; per-patch applied/unchanged
verdicts are part of the contract.
- shared/fixtures/pagination/ (11 cases): op scripts driving the real
message-window store with a scripted ApiClient — latest page + SSE ingest,
before-cursor older pages, epoch-mismatch reset (window discard + recorded
internal latest request), reset:true replace preserving optimistic rows,
localId echo reconciliation, messages-consumed invokedAt stamping (no
cursor advance), message-cancelled removal, cancel-too-late invoked-row
ingest, hidden-row cursor advance, 400-row trim preserving queued rows,
and queued-state gap recovery. Documents pin the exact requests the store
issued, older-load outcomes, reconcile candidates, and a minimal window
projection (ids/order, queued/optimistic flags, hasMore, epoch, viewMode,
compound cursors).
Generator: web/scripts/fixtures/{sse,pagination}/ extend the K5 framework
(canonical serialization reused; suites pruned of stale files). Web
self-conformance tests replay every fixture against the real implementation
in bun run test:web. README documents schemas, projections, replay
contracts, and native consumption for both suites.
No time injection needed: the store's only Date.now() reads gate
notification throttling, which never reaches persisted or projected state.
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
import { readdirSync, readFileSync } from 'node:fs'
|
||||
import { join, resolve } from 'node:path'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { clearMessageWindow } from '@/lib/message-window-store'
|
||||
import { FIXTURE_VERSION } from '../../scripts/fixtures/fixtureTypes'
|
||||
import {
|
||||
extractOpExpectations,
|
||||
paginationFixtureSessionId,
|
||||
runPaginationScript
|
||||
} from '../../scripts/fixtures/pagination/runner'
|
||||
import type { PaginationFixtureDocument } from '../../scripts/fixtures/pagination/types'
|
||||
import { toCanonicalJson } from '../../scripts/fixtures/serialize'
|
||||
|
||||
// Self-conformance for the pagination fixtures in shared/fixtures/pagination:
|
||||
// replaying every stored op script through the REAL message-window store must
|
||||
// reproduce the stored request log, older-load outcomes, reconcile candidates
|
||||
// and final window projection, and every file must be canonically serialized.
|
||||
// A failure here means the web store (or the projection) drifted — rerun
|
||||
// `bun run gen:fixtures` from the repo root and commit the diff, which is what
|
||||
// flips the native iOS/Android conformance suites red.
|
||||
|
||||
const fixturesDir = resolve(process.cwd(), '../shared/fixtures')
|
||||
const paginationDir = join(fixturesDir, 'pagination')
|
||||
const fixtureFiles = readdirSync(paginationDir).filter((name) => name.endsWith('.json')).sort()
|
||||
|
||||
function canonical(value: unknown): unknown {
|
||||
return JSON.parse(toCanonicalJson(value))
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
for (const file of fixtureFiles) {
|
||||
clearMessageWindow(paginationFixtureSessionId(file.slice(0, -'.json'.length)))
|
||||
}
|
||||
sessionStorage.clear()
|
||||
})
|
||||
|
||||
describe('shared/fixtures/pagination golden fixtures', () => {
|
||||
it('has generated fixtures on disk', () => {
|
||||
expect(fixtureFiles.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('VERSION matches the generator fixtureVersion', () => {
|
||||
expect(readFileSync(join(fixturesDir, 'VERSION'), 'utf8')).toBe(`${FIXTURE_VERSION}\n`)
|
||||
})
|
||||
|
||||
for (const file of fixtureFiles) {
|
||||
describe(file, () => {
|
||||
const raw = readFileSync(join(paginationDir, file), 'utf8')
|
||||
const document = JSON.parse(raw) as PaginationFixtureDocument
|
||||
|
||||
it('is canonically serialized (sorted keys, 4-space indent, trailing LF)', () => {
|
||||
expect(toCanonicalJson(document)).toBe(raw)
|
||||
})
|
||||
|
||||
it('carries the current fixtureVersion and a name matching its file', () => {
|
||||
expect(document.fixtureVersion).toBe(FIXTURE_VERSION)
|
||||
expect(`${document.name}.json`).toBe(file)
|
||||
})
|
||||
|
||||
it('replaying the ops against the real store matches the stored expectations', async () => {
|
||||
const { observations, expectedState } = await runPaginationScript(
|
||||
paginationFixtureSessionId(document.name),
|
||||
document.ops
|
||||
)
|
||||
expect(canonical(observations)).toEqual(canonical(extractOpExpectations(document.ops)))
|
||||
expect(canonical(expectedState)).toEqual(canonical(document.expectedState))
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,71 @@
|
||||
import { readdirSync, readFileSync } from 'node:fs'
|
||||
import { join, resolve } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { SessionPatchSchema, SessionSchema } from '@hapi/protocol/schemas'
|
||||
import { FIXTURE_VERSION } from '../../scripts/fixtures/fixtureTypes'
|
||||
import { toCanonicalJson } from '../../scripts/fixtures/serialize'
|
||||
import { runSessionPatchScript } from '../../scripts/fixtures/sse/apply'
|
||||
import type { SseFixtureDocument } from '../../scripts/fixtures/sse/types'
|
||||
|
||||
// Self-conformance for the SSE session-patch fixtures in shared/fixtures/sse:
|
||||
// the stored expectations must equal a fresh applySessionDetailPatch fold over
|
||||
// the stored initialSession + patches, every input must round-trip through the
|
||||
// wire schemas unchanged, and every file must be canonically serialized. A
|
||||
// failure here means the web patch rules (or the schemas) drifted — rerun
|
||||
// `bun run gen:fixtures` from the repo root and commit the diff, which is what
|
||||
// flips the native iOS/Android conformance suites red.
|
||||
|
||||
const fixturesDir = resolve(process.cwd(), '../shared/fixtures')
|
||||
const sseDir = join(fixturesDir, 'sse')
|
||||
const fixtureFiles = readdirSync(sseDir).filter((name) => name.endsWith('.json')).sort()
|
||||
|
||||
function canonical(value: unknown): unknown {
|
||||
return JSON.parse(toCanonicalJson(value))
|
||||
}
|
||||
|
||||
describe('shared/fixtures/sse golden fixtures', () => {
|
||||
it('has generated fixtures on disk', () => {
|
||||
expect(fixtureFiles.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('VERSION matches the generator fixtureVersion', () => {
|
||||
expect(readFileSync(join(fixturesDir, 'VERSION'), 'utf8')).toBe(`${FIXTURE_VERSION}\n`)
|
||||
})
|
||||
|
||||
for (const file of fixtureFiles) {
|
||||
describe(file, () => {
|
||||
const raw = readFileSync(join(sseDir, file), 'utf8')
|
||||
const document = JSON.parse(raw) as SseFixtureDocument
|
||||
|
||||
it('is canonically serialized (sorted keys, 4-space indent, trailing LF)', () => {
|
||||
expect(toCanonicalJson(document)).toBe(raw)
|
||||
})
|
||||
|
||||
it('carries the current fixtureVersion and a name matching its file', () => {
|
||||
expect(document.fixtureVersion).toBe(FIXTURE_VERSION)
|
||||
expect(`${document.name}.json`).toBe(file)
|
||||
})
|
||||
|
||||
it('stores schema-normalized wire inputs (SessionSchema / strict SessionPatchSchema)', () => {
|
||||
// Parsing the stored form must be a no-op: natives can decode
|
||||
// the document verbatim, and a wire-schema change that would
|
||||
// alter parsing shows up as a diff here.
|
||||
expect(canonical(SessionSchema.parse(document.initialSession)))
|
||||
.toEqual(canonical(document.initialSession))
|
||||
expect(document.patches.length).toBeGreaterThan(0)
|
||||
for (const patch of document.patches) {
|
||||
expect(canonical(SessionPatchSchema.parse(patch))).toEqual(canonical(patch))
|
||||
}
|
||||
})
|
||||
|
||||
it('expected outcome matches a fresh applySessionDetailPatch fold over the inputs', () => {
|
||||
const { expectedPatchResults, expectedSession } = runSessionPatchScript(
|
||||
document.initialSession,
|
||||
document.patches
|
||||
)
|
||||
expect(expectedPatchResults).toEqual(document.expectedPatchResults)
|
||||
expect(canonical(expectedSession)).toEqual(canonical(document.expectedSession))
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user