Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions .github/workflows/browser.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: Deterministic documentation browser flows
on:
pull_request:
push:
branches: [main]
permissions:
contents: read
concurrency:
group: browser-${{ github.ref }}
cancel-in-progress: true
jobs:
browser:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
persist-credentials: false
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: 24.8.0
- run: npm ci
- run: npm ci --prefix tests/browser
- run: npx tsc --project tests/browser/tsconfig.json
- run: npm run docs:check
- run: npm run docs:build
- run: npx --no-install e2e-web install chromium --with-deps
working-directory: tests/browser
- run: npm test
working-directory: tests/browser
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
if: always()
with:
name: documentation-browser-evidence
path: tests/browser/.e2e/
include-hidden-files: true
retention-days: 7
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,9 @@
/docs/next-env.d.ts
/docs/tsconfig.tsbuildinfo
/*.tgz

# Deterministic browser traces
tests/browser/.e2e/

# Isolated browser-tool dependencies
tests/browser/node_modules/
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,3 +236,33 @@ FontTools interoperability check in both directions.
## license

[MIT](LICENSE)

## Deterministic browser verification

The documentation UI is tested with pinned `tester-army/e2e` packages in an
isolated private tooling package; the published CLI's Node >=18 requirement is
unchanged. With Node >=24.8 for browser tooling:

```sh
npm ci
npm ci --prefix tests/browser
npm run docs:check
npm run docs:build
cd tests/browser
npx --no-install e2e-web install chromium --with-deps
npm test
```

The suite opens all documentation pages, retrieves their Markdown endpoints,
and checks mobile layout. It makes no model calls and uses no font-provider
credentials. Native `go test ./cmd/... ./internal/... ./e2e/...` additionally
runs the compiled CLI, real loopback font-download fixtures, conversion and PTY
flows. Live third-party provider availability is a separate acceptance surface.

Ranking now memoizes query relevance by filename/name within each call. The
frozen-reference regression compares every result, score and ordering across
24 query/weight combinations and checks that caller input stays unchanged.
On a synthetic 600-row fixture, three-run median time was 13.51 ms before versus
1.66 ms after; allocations fell from about 67,666 to 7,263 per operation. This
is a focused ranking benchmark, not an end-to-end search/network speed claim.
Reproduce with `go test ./internal/rank -bench BenchmarkRanking -benchmem`.
2 changes: 1 addition & 1 deletion internal/provider/github_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -544,7 +544,7 @@ func TestGitHubRetryAfter(t *testing.T) {
want time.Duration
}{
{"seconds", http.Header{"Retry-After": {"7"}}, 7 * time.Second},
{"date", http.Header{"Retry-After": {now.Add(9 * time.Second).Format(http.TimeFormat)}}, 9 * time.Second},
{"date", http.Header{"Retry-After": {now.Add(9 * time.Second).UTC().Format(http.TimeFormat)}}, 9 * time.Second},
{"reset", http.Header{"X-Ratelimit-Reset": {fmt.Sprint(now.Add(11 * time.Second).Unix())}}, 11 * time.Second},
{"retry after wins", http.Header{"Retry-After": {"7"}, "X-Ratelimit-Reset": {fmt.Sprint(now.Add(11 * time.Second).Unix())}}, 7 * time.Second},
}
Expand Down
126 changes: 126 additions & 0 deletions internal/rank/cache_benchmark_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package rank

import (
"fmt"
"github.com/microck/moji/internal/provider"
"math"
"reflect"
"sort"
"strings"
"testing"
)

// Frozen pre-optimization implementation, retained to compare full ordering,
// scores, and metadata rather than only the first result.
func baselineResults(results []provider.Result, query, wantedWeight string, weights Weights) []provider.Result {
normalizedQuery := strings.ToLower(NormalizeQuery(query))
compactQuery := strings.ReplaceAll(normalizedQuery, " ", "")
ranked := make([]provider.Result, 0, len(results))
for _, result := range results {
if compactQuery == "" || relevance(result, normalizedQuery, compactQuery) > 0 {
ranked = append(ranked, result)
}
}
familySizes := make(map[string]map[string]struct{})
for i := range ranked {
tags := ParseFilename(ranked[i].Filename)
if ranked[i].Format == "" {
ranked[i].Format = tags.Format
}
if ranked[i].Weight == "" {
ranked[i].Weight = tags.Weight
}
if tags.Variable {
ranked[i].Variable = true
}
key := tags.Family + "\x00" + familyGroupOf(ranked[i])
if familySizes[key] == nil {
familySizes[key] = make(map[string]struct{})
}
if ranked[i].Weight != "" {
familySizes[key][ranked[i].Weight] = struct{}{}
}
}
for i := range ranked {
tags := ParseFilename(ranked[i].Filename)
formatRank := map[string]float64{"otf": 3, "ttf": 2, "dfont": 1.5, "pfb": 1, "woff2": 0.75, "woff": 0.5, "pfm": 0.1}[ranked[i].Format]
score := weights.Format * formatRank
score += weights.FamilySize * math.Log2(1+float64(len(familySizes[tags.Family+"\x00"+familyGroupOf(ranked[i])])))
if ranked[i].Trusted {
score += weights.Trusted
}
if wantedWeight != "" && ranked[i].Weight == wantedWeight {
score += weights.WeightBonus * 3
}
if ranked[i].Variable {
score += weights.VariableBonus
}
if ranked[i].SizeBytes > 0 && ranked[i].SizeBytes < 10_000 {
score -= weights.SizePenalty
}
ranked[i].Score = score
}
sort.Slice(ranked, func(i, j int) bool {
leftRelevance := relevance(ranked[i], normalizedQuery, compactQuery)
rightRelevance := relevance(ranked[j], normalizedQuery, compactQuery)
if leftRelevance != rightRelevance {
return leftRelevance > rightRelevance
}
if ranked[i].Score != ranked[j].Score {
return ranked[i].Score > ranked[j].Score
}
leftReliability := sourceReliability(ranked[i])
rightReliability := sourceReliability(ranked[j])
if leftReliability != rightReliability {
return leftReliability > rightReliability
}
if ranked[i].Filename != ranked[j].Filename {
return ranked[i].Filename < ranked[j].Filename
}
if ranked[i].Source != ranked[j].Source {
return ranked[i].Source < ranked[j].Source
}
return ranked[i].URL < ranked[j].URL
})
return ranked
}

func rankingFixture() []provider.Result {
results := make([]provider.Result, 600)
families := []string{"Inter", "Inter var", "Example", "Font", "SourceSans", "AvenirNext", "ProximaNova", "HelveticaNeue"}
weights := []string{"Regular", "Bold", "Light", "SemiBoldItalic"}
for i := range results {
results[i] = provider.Result{Name: families[i%len(families)], Filename: fmt.Sprintf("%s-%s.ttf", families[i%len(families)], weights[(i/7)%len(weights)]), Source: fmt.Sprintf("source-%d", i%9), URL: fmt.Sprintf("https://example.test/font-%d.ttf", i), SizeBytes: int64(8000 + i*10), Trusted: i%3 == 0}
}
return results
}
func TestCachedRankingPreservesCompleteResults(t *testing.T) {
input := rankingFixture()
before := append([]provider.Result(nil), input...)
for _, query := range []string{"", "Inter", "Source Sans", "Avenir", "unmatched", "FONT", "Proxima Nova", "Helvetic"} {
for _, weight := range []string{"", "bold", "light"} {
got := Results(input, query, weight, DefaultWeights())
want := baselineResults(input, query, weight, DefaultWeights())
if !reflect.DeepEqual(got, want) {
t.Fatalf("ranking changed for %q/%q", query, weight)
}
}
}
if !reflect.DeepEqual(input, before) {
t.Fatal("ranking mutated caller results")
}
}
func BenchmarkRankingBaseline(b *testing.B) {
input := rankingFixture()
b.ReportAllocs()
for b.Loop() {
baselineResults(input, "Inter", "", DefaultWeights())
}
}
func BenchmarkRankingCached(b *testing.B) {
input := rankingFixture()
b.ReportAllocs()
for b.Loop() {
Results(input, "Inter", "", DefaultWeights())
}
}
19 changes: 16 additions & 3 deletions internal/rank/rank.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,9 +178,22 @@ func AdaptiveQueries(query string) []string {
func Results(results []provider.Result, query, wantedWeight string, weights Weights) []provider.Result {
normalizedQuery := strings.ToLower(NormalizeQuery(query))
compactQuery := strings.ReplaceAll(normalizedQuery, " ", "")
// Relevance depends only on these two identity fields for this query.
// Sorting compares each result repeatedly, so parse each identity once.
type identity struct{ filename, name string }
relevanceByIdentity := make(map[identity]int, len(results))
cachedRelevance := func(result provider.Result) int {
key := identity{result.Filename, result.Name}
if score, ok := relevanceByIdentity[key]; ok {
return score
}
score := relevance(result, normalizedQuery, compactQuery)
relevanceByIdentity[key] = score
return score
}
ranked := make([]provider.Result, 0, len(results))
for _, result := range results {
if compactQuery == "" || relevance(result, normalizedQuery, compactQuery) > 0 {
if compactQuery == "" || cachedRelevance(result) > 0 {
ranked = append(ranked, result)
}
}
Expand Down Expand Up @@ -224,8 +237,8 @@ func Results(results []provider.Result, query, wantedWeight string, weights Weig
ranked[i].Score = score
}
sort.Slice(ranked, func(i, j int) bool {
leftRelevance := relevance(ranked[i], normalizedQuery, compactQuery)
rightRelevance := relevance(ranked[j], normalizedQuery, compactQuery)
leftRelevance := cachedRelevance(ranked[i])
rightRelevance := cachedRelevance(ranked[j])
if leftRelevance != rightRelevance {
return leftRelevance > rightRelevance
}
Expand Down
31 changes: 31 additions & 0 deletions tests/browser/docs.e2e.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {readFileSync,readdirSync} from 'node:fs';
import {join,relative,sep} from 'node:path';
import {parse} from 'yaml';
import {test} from '@e2e-dev/web';
import {expect} from 'e2e';
const root='../../docs/content/docs';
function pages(directory=root):{route:string;title:string}[]{
return readdirSync(directory,{withFileTypes:true}).flatMap(entry=>{
const path=join(directory,entry.name);
if(entry.isDirectory())return pages(path);
if(!/\.mdx?$/.test(path))return [];
const front=readFileSync(path,'utf8').match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/)?.[1];
const data=front===undefined?null:parse(front);
if(typeof data?.title!=='string'||!data.title.trim())throw new Error(`Missing title: ${path}`);
return [{title:data.title,route:'/docs/'+relative(root,path).split(sep).join('/').replace(/\.mdx?$/,'').replace(/(^|\/)index$/,'')}];
});
}
for(const page of pages())test(`rendered documentation ${page.route}`,async({app,screen,browser})=>{
await app.open(page.route);
await expect(screen.getByRole('heading',{name:page.title,exact:true})).toBeVisible();
const markdown=await browser.evaluate(async path=>{const response=await fetch('/llms.mdx'+path+'/content.md');return {status:response.status,text:await response.text()};},page.route.replace(/\/$/,''));
expect(markdown.status).toBe(200);
expect(markdown.text.length).toBeGreaterThan(20);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
expect(markdown.text.startsWith(`# ${page.title} (${page.route.replace(/\/$/,'')})\n`)).toBe(true);
});
test('mobile docs navigation and readable content',async({app,browser,screen})=>{
await browser.setViewport({width:390,height:844});
await app.open('/docs');
await expect(screen.getByRole('heading',{name:pages().find(p=>p.route==='/docs/')!.title,exact:true})).toBeVisible();
expect(await browser.evaluate(()=>document.documentElement.scrollWidth<=innerWidth+1)).toBe(true);
});
3 changes: 3 additions & 0 deletions tests/browser/e2e.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import type {E2EConfig} from 'e2e';
import {web} from '@e2e-dev/web';
export default {tests:'*.e2e.ts',workers:1,retries:0,trace:'on',timeout:30000,targets:[{name:'chromium',engine:web(),app:{url:'http://127.0.0.1:4192',command:{executable:'node',args:['serve.mjs'],log:'.e2e/server.log'}}}]} satisfies E2EConfig;
Loading
Loading