-
Notifications
You must be signed in to change notification settings - Fork 4
test(dogfood): ADR-0061 search-conformance ledger + HTTP proof (flip to Accepted); ADR-0028 → Deferred #3065
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 2 commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. | ||
|
|
||
| import type { ConformanceRow } from '@objectstack/verify'; | ||
| // | ||
| // ADR-0061 §Conformance — Record-Search Conformance ledger (ADR-0060 pattern). | ||
| // | ||
| // The ADR's own landing bar: `$search` is "landed" iff a driver-reachable | ||
| // executor runs it AND a dogfood proof asserts a MULTI-FIELD match over the | ||
| // real HTTP API (e.g. searching "retail" returns an account matched by | ||
| // `industry`, not `name`). This ledger is the durable encoding of that bar: | ||
| // every search-related declarable surface sits in exactly one honest state | ||
| // (ADR-0049), names its executor site, and the enforced rows reference the | ||
| // HTTP-level proof (`showcase-search.dogfood.test.ts`). | ||
| // | ||
| // Tier 2 (FTS / relevance / external engines) is deliberately ABSENT from this | ||
| // ledger rather than carried as an aspirational row — per ADR-0049 a surface | ||
| // that does not exist in the spec cannot be "declared but unenforced". When a | ||
| // driver grows `fullTextSearch: true`, add its row here with a proof. | ||
|
|
||
| export const SEARCH_SURFACE: ConformanceRow[] = [ | ||
| { | ||
| id: 'search-executor', | ||
| summary: '`$search` server-resolved cross-field executor (terms AND-ed, fields OR-ed, case-insensitive `$contains`)', | ||
| surface: 'spec/api/query.zod.ts:$search (QueryParams `search`)', | ||
| state: 'enforced', | ||
| enforcement: 'objectql/src/engine.ts (find AST expansion) → objectql/src/search-filter.ts expandSearchToFilter', | ||
| proof: 'showcase-search.dogfood.test.ts', | ||
| }, | ||
| { | ||
| id: 'searchable-fields-object', | ||
| summary: '`object.searchableFields` — canonical allowed-search-field set; auto-default (name/title + short text, secret/PII/json excluded) when unset', | ||
| surface: 'spec/data/object.zod.ts:searchableFields', | ||
| state: 'enforced', | ||
| enforcement: 'objectql/src/search-filter.ts resolveSearchFields / autoDefaultFields', | ||
| proof: 'showcase-search.dogfood.test.ts', | ||
| }, | ||
| { | ||
| id: 'search-fields-override', | ||
| summary: '`$searchFields` per-query narrowing — validated against the allowed set, can never widen it', | ||
| surface: 'spec/api/query.zod.ts:$searchFields', | ||
| state: 'enforced', | ||
| enforcement: 'objectql/src/search-filter.ts resolveSearchFields (intersection)', | ||
| proof: 'showcase-search.dogfood.test.ts', | ||
| }, | ||
| { | ||
| id: 'search-select-label-mapping', | ||
| summary: 'select/status label→value mapping — a human label in the query matches option values', | ||
| surface: 'spec/data/field.zod.ts:options (select) × $search', | ||
| state: 'enforced', | ||
| enforcement: 'objectql/src/search-filter.ts optionValuesMatching / fieldClausesForTerm', | ||
| proof: 'showcase-search.dogfood.test.ts', | ||
| }, | ||
| ]; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. | ||
| // | ||
| // ADR-0061 §Conformance — the Record-Search Conformance ledger is a CHECKED | ||
| // artifact (ADR-0060 `checkLedger`): shared invariants + every enforced row | ||
| // must carry an existing proof file. The proof itself | ||
| // (`showcase-search.dogfood.test.ts`) asserts the ADR's landing bar over the | ||
| // real HTTP API; this test asserts the ledger cannot silently rot (a renamed | ||
| // or deleted proof file fails here, closing the declared-but-unenforced loop). | ||
|
|
||
| import { describe, expect, it } from 'vitest'; | ||
| import { dirname, join } from 'node:path'; | ||
Check noticeCode scanning / CodeQL Unused variable, import, function or class Note test
Unused import join.
|
||
| import { fileURLToPath } from 'node:url'; | ||
| import { checkLedger } from '@objectstack/verify'; | ||
| import { SEARCH_SURFACE } from './search-conformance.ledger.js'; | ||
|
|
||
| const HERE = dirname(fileURLToPath(import.meta.url)); | ||
|
|
||
| describe('ADR-0061 record-search conformance ledger', () => { | ||
| it('is sound and every enforced row carries an existing proof', () => { | ||
| const problems = checkLedger(SEARCH_SURFACE, { | ||
| proofRoot: HERE, | ||
| proofRequiredForEnforced: true, | ||
| }); | ||
| expect(problems).toEqual([]); | ||
| }); | ||
|
|
||
| it('covers the executor surface exactly once', () => { | ||
| const ids = SEARCH_SURFACE.map((r) => r.id); | ||
| expect(new Set(ids).size).toBe(ids.length); | ||
| expect(ids).toContain('search-executor'); | ||
| }); | ||
| }); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. | ||
| // | ||
| // ADR-0061 §Conformance — the dogfood proof behind the search-conformance | ||
| // ledger: a MULTI-FIELD `$search` match over the REAL HTTP API. The ADR's own | ||
| // example, verbatim: searching "retail" must return an account matched by | ||
| // `industry`, not `name` (the showcase seed's Northwind row carries | ||
| // `industry: 'retail'` and no account name contains "retail"). Also proves | ||
| // the `$searchFields` narrowing (can only narrow, never widen) so the | ||
| // executor's security posture is pinned at the HTTP level, not just in | ||
| // `search-filter.test.ts` unit tests. | ||
|
|
||
| import { describe, it, expect, beforeAll, afterAll } from 'vitest'; | ||
| import showcaseStack from '@objectstack/example-showcase'; | ||
| import { bootStack, type VerifyStack } from '@objectstack/verify'; | ||
|
|
||
| describe('showcase: $search over the HTTP API (ADR-0061 conformance proof)', () => { | ||
| let stack: VerifyStack; | ||
| let token: string; | ||
|
|
||
| const query = async (body: Record<string, unknown>) => { | ||
| const res = await stack.apiAs(token, 'POST', '/data/showcase_account/query', body); | ||
| expect(res.status).toBe(200); | ||
| const data = await res.json(); | ||
| return (data?.data?.records ?? data?.records ?? []) as Array<Record<string, unknown>>; | ||
| }; | ||
|
|
||
| beforeAll(async () => { | ||
| stack = await bootStack(showcaseStack); | ||
| token = await stack.signIn(); | ||
| }, 60_000); | ||
| afterAll(async () => { await stack?.stop(); }); | ||
|
|
||
| it('multi-field match: "retail" returns Northwind via industry, not name', async () => { | ||
| const records = await query({ search: 'retail' }); | ||
| const names = records.map((r) => String(r.name)); | ||
| expect(names).toContain('Northwind'); | ||
| // Guard the premise: Northwind's NAME does not contain "retail", so its | ||
| // presence in the result can only come from a non-name field… | ||
| expect(names.find((n) => n === 'Northwind')!.toLowerCase()).not.toContain('retail'); | ||
| // …and prove it positively: restricting the same search to `industry` | ||
| // alone still returns Northwind — the hit IS the industry field. | ||
| const viaIndustry = await query({ search: 'retail', searchFields: ['industry'] }); | ||
| expect(viaIndustry.map((r) => r.name)).toContain('Northwind'); | ||
| // (The seed also has an account literally NAMED "acme retail" — a useful | ||
| // control: the unrestricted search returns it via `name`, proving the | ||
| // cross-field OR spans both fields in one query.) | ||
| expect(names.some((n) => n.toLowerCase().includes('retail'))).toBe(true); | ||
| }); | ||
|
|
||
| it('select label→value mapping: the capitalized label "Retail" also matches', async () => { | ||
| const records = await query({ search: 'Retail' }); | ||
| expect(records.map((r) => r.name)).toContain('Northwind'); | ||
| }); | ||
|
|
||
| it('$searchFields narrows: "retail" restricted to name matches nothing', async () => { | ||
| const records = await query({ search: 'retail', searchFields: ['name'] }); | ||
| expect(records.map((r) => r.name)).not.toContain('Northwind'); | ||
| }); | ||
|
|
||
| it('terms AND: "retail northwind" still matches; "retail contoso" does not', async () => { | ||
| const both = await query({ search: 'retail northwind' }); | ||
| expect(both.map((r) => r.name)).toContain('Northwind'); | ||
| const cross = await query({ search: 'retail contoso' }); | ||
| expect(cross.map((r) => r.name)).not.toContain('Northwind'); | ||
| expect(cross.map((r) => r.name)).not.toContain('Contoso'); | ||
| }); | ||
| }); |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.