Skip to content

Commit 2fa94aa

Browse files
Merge pull request #2 from nicolo-ribaudo/merge-2026-04-15
This branch was the result of merging `eurosky-social/main` (242f472) into `bluesky-social/main` (ff9f84e).
2 parents 242f472 + c8b730b commit 2fa94aa

1,762 files changed

Lines changed: 53987 additions & 118345 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/skills/backend-lexification/SKILL.md

Lines changed: 904 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
---
2+
name: vitest-patterns
3+
description: Patterns and conventions for writing vitest tests in this project. This skill should be used when writing new tests, adding test cases to existing test files, or reviewing test code for correctness. Trigger whenever the user asks to write tests, add test coverage, create test files, or mentions vitest/testing.
4+
---
5+
6+
# Vitest Test Patterns
7+
8+
Conventions and patterns for writing vitest tests in this codebase.
9+
10+
## Imports
11+
12+
Always import test utilities from `vitest`. Use named imports:
13+
14+
```ts
15+
import { assert, describe, expect, it, vi } from 'vitest'
16+
```
17+
18+
Only import what you use. Add `vi` only when using mock functions. Add `assert` only when narrowing types.
19+
20+
## Test Structure
21+
22+
### Describe labels
23+
24+
Pass the function/class under test directly as the `describe` label when possible. Use string labels for conceptual groupings:
25+
26+
```ts
27+
// Function reference as label (preferred when testing a single export)
28+
describe(parseCid, () => { ... })
29+
describe(isLexValue, () => { ... })
30+
31+
// String label for conceptual groups or when testing multiple related behaviors
32+
describe('roundtrip toBase64 <-> fromBase64', () => { ... })
33+
describe('isObject', () => { ... })
34+
```
35+
36+
### Grouping
37+
38+
Nest logical groupings inside the top-level describe. Common groupings:
39+
40+
```ts
41+
describe(someFunction, () => {
42+
describe('valid inputs', () => { ... })
43+
describe('invalid inputs', () => { ... })
44+
describe('edge cases', () => { ... })
45+
})
46+
```
47+
48+
For features with a default behavior and an override, cover both:
49+
50+
```ts
51+
describe('validateResponse', () => {
52+
it('rejects invalid response body by default', ...)
53+
it('accepts invalid response body when disabled', ...)
54+
it('succeeds with valid response body when enabled', ...)
55+
})
56+
```
57+
58+
For safety-critical code, group edge cases under a `'safety'` or `'edge cases'` describe:
59+
60+
```ts
61+
describe('safety', () => {
62+
it('handles cyclic structures without infinite loops', () => { ... })
63+
it('handles deep structures without exceeding call stack', () => { ... })
64+
})
65+
```
66+
67+
## Parameterized Tests
68+
69+
Use `for...of` loops over test case arrays instead of `it.each`:
70+
71+
```ts
72+
describe(isLexScalar, () => {
73+
for (const { note, value, expected } of [
74+
{ note: 'string', value: 'hello', expected: true },
75+
{ note: 'boolean', value: true, expected: true },
76+
{ note: 'null', value: null, expected: true },
77+
{ note: 'number (float)', value: 3.14, expected: false },
78+
{ note: 'undefined', value: undefined, expected: false },
79+
]) {
80+
it(note, () => {
81+
expect(isLexScalar(value)).toBe(expected)
82+
})
83+
}
84+
})
85+
```
86+
87+
This also works for running the same test suite against multiple implementations:
88+
89+
```ts
90+
for (const utf8Len of [utf8LenNode!, utf8LenCompute!] as const) {
91+
describe(utf8Len, () => {
92+
it('computes utf8 string length', () => {
93+
expect(utf8Len('a')).toBe(1)
94+
})
95+
})
96+
}
97+
```
98+
99+
## Assertions
100+
101+
### Type narrowing with `assert`
102+
103+
Use `assert()` from vitest for type narrowing and boolean checks. **Always prefer `assert(result.success)` over `expect(result.success).toBe(true)`** — the `assert` provides type narrowing in TypeScript, which allows the rest of the test to access narrowed properties without additional type guards.
104+
105+
```ts
106+
// Narrow to a specific type before further assertions
107+
assert(err instanceof XrpcFetchError)
108+
expect(err.cause).toBeInstanceOf(TypeError)
109+
110+
// Discriminated union checks - PREFERRED
111+
assert(result.success)
112+
expect(result.body).toEqual({ value: 'hello' })
113+
// TypeScript now knows result.body exists
114+
115+
assert(!result.success)
116+
expect(result).toBeInstanceOf(XrpcResponseError)
117+
// TypeScript now knows result has error properties
118+
119+
// DON'T do this - it doesn't narrow types
120+
expect(result.success).toBe(true) // ❌ No type narrowing
121+
if (result.success) {
122+
expect(result.body).toEqual({ value: 'hello' }) // Still need type guard
123+
}
124+
```
125+
126+
### Error assertions with `rejects.toSatisfy`
127+
128+
For thrown errors, prefer `rejects.toSatisfy()` over `rejects.toThrow()` when you need multiple detailed assertions:
129+
130+
```ts
131+
await expect(
132+
someAsyncFn(),
133+
).rejects.toSatisfy((err) => {
134+
assert(err instanceof SomeError)
135+
expect(err.cause).toBeInstanceOf(TypeError)
136+
expect(err.message).toContain('failed')
137+
return true // must return true
138+
})
139+
```
140+
141+
Always `return true` at the end of `toSatisfy` callbacks.
142+
143+
For simple "it throws" checks, `toThrow()` is fine:
144+
145+
```ts
146+
expect(() => parseCid(invalidStr)).toThrow()
147+
expect(() => cidForRawHash(new Uint8Array(31))).toThrow('Invalid SHA-256 hash length')
148+
```
149+
150+
## Mock Functions
151+
152+
### Use `vi.fn()` with type parameters
153+
154+
When you need to inspect how a function was called, use `vi.fn<Type>()`:
155+
156+
```ts
157+
const fetchHandler = vi.fn<FetchHandler>(async () =>
158+
Response.json({ value: 'ok' }),
159+
)
160+
161+
await xrpc(fetchHandler, testQuery, { params: { limit: 25 } })
162+
163+
expect(fetchHandler).toHaveBeenCalledOnce()
164+
const [path, init] = fetchHandler.mock.calls[0]
165+
expect(path).toContain('/xrpc/io.example.testQuery')
166+
```
167+
168+
When you don't need to inspect calls, use a plain typed function:
169+
170+
```ts
171+
const fetchHandler: FetchHandler = async () => Response.json({ value: 'hello' })
172+
```
173+
174+
## Test Fixtures
175+
176+
Define reusable fixtures at the top of the file, outside describe blocks:
177+
178+
```ts
179+
const invalidCidStr = 'invalidcidstring'
180+
const cborCidStr = 'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a'
181+
const cborCid = parseCid(cborCidStr, { flavor: 'cbor' })
182+
```
183+
184+
Keep fixtures minimal and focused on what the tests need.
185+
186+
## TypeScript in Tests
187+
188+
### Intentionally invalid arguments
189+
190+
Use `// @ts-expect-error` with a description:
191+
192+
```ts
193+
await xrpc(fetchHandler, testQuery, {
194+
// @ts-expect-error intentionally passing invalid params
195+
params: { limit: 'not-a-number' },
196+
validateRequest: true,
197+
})
198+
```
199+
200+
## Global Stubbing
201+
202+
When testing code that uses a global, temporarily replace it and restore in a `finally` block:
203+
204+
```ts
205+
it('throws TypeError when fetch is not available', () => {
206+
const originalFetch = globalThis.fetch
207+
try {
208+
// @ts-expect-error removing fetch to simulate missing environment
209+
globalThis.fetch = undefined
210+
expect(() => buildAgent({ service: 'https://example.com' })).toThrow(TypeError)
211+
} finally {
212+
globalThis.fetch = originalFetch
213+
}
214+
})
215+
```
216+
217+
Use `try/finally` (not `beforeEach`/`afterEach`) when the stub is scoped to a single test.
218+
219+
## Roundtrip Tests
220+
221+
When testing encode/decode or serialize/deserialize pairs, add a dedicated roundtrip describe:
222+
223+
```ts
224+
describe('roundtrip toBase64 <-> fromBase64', () => {
225+
it('roundtrips empty array', () => {
226+
const original = new Uint8Array(0)
227+
expect(ui8Equals(fromBase64(toBase64(original)), original)).toBe(true)
228+
})
229+
230+
it('roundtrips all byte values', () => {
231+
const allBytes = new Uint8Array(256)
232+
for (let i = 0; i < 256; i++) allBytes[i] = i
233+
expect(ui8Equals(fromBase64(toBase64(allBytes)), allBytes)).toBe(true)
234+
})
235+
})
236+
```
237+
238+
## Workflow
239+
240+
- Do not worry about code formatting or lint issues. The user will review changes in their editor, which applies formatting automatically on save.
241+
- Do not commit test changes. Leave them as unstaged modifications for the user to review.
242+
243+
## Running Tests
244+
245+
```bash
246+
pnpm exec vitest run path/to/file.test.ts
247+
```

.eslintignore

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,18 @@ packages/bsync/src/proto
77

88
# codegen
99
packages/api/src/client
10-
packages/lexicon-resolver/src/client
11-
packages/bsky/src/lexicon
12-
packages/pds/src/lexicon
1310
packages/ozone/src/lexicon
1411

1512
# @atproto/lex
13+
packages/lexicon-resolver/src/lexicons
1614
packages/lex/*/src/lexicons
1715
packages/lex/*/tests/lexicons
1816
packages/oauth/oauth-client-browser-example/src/lexicons
17+
packages/pds/src/lexicons
18+
packages/bsky/src/lexicons
19+
packages/sync/src/lexicons
1920

2021
# others
22+
packages/api/src/moderation/const/labels.ts
2123
packages/oauth/*/src/locales/*/messages.ts
22-
packages/oauth/oauth-provider-frontend/src/routeTree.gen.ts
2324
packages/oauth/oauth-client-expo/android/build

.eslintrc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@
3333
"distinctGroup": true,
3434
"alphabetize": { "order": "asc" },
3535
"newlines-between": "never",
36+
"pathGroups": [
37+
{ "pattern": "#/**", "group": "parent", "position": "before" }
38+
],
3639
"groups": [
3740
"builtin",
3841
"external",

.gitattributes

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ packages/bsync/src/proto/** linguist-generated=true
44

55
# codegen
66
packages/api/src/client/** linguist-generated=true
7-
packages/lexicon-resolver/src/client/** linguist-generated=true
8-
packages/bsky/src/lexicon/** linguist-generated=true
9-
packages/pds/src/lexicon/** linguist-generated=true
107
packages/ozone/src/lexicon/** linguist-generated=true
118

9+
# @atproto/lex
10+
packages/lexicon-resolver/src/lexicons/** linguist-generated=true
11+
1212
# i18n
1313
packages/oauth/oauth-provider-ui/src/locales/**/messages.po linguist-generated=true

.github/workflows/build-and-push-bsky-aws.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ on:
33
push:
44
branches:
55
- main
6+
- msi/pds-lexification
7+
68
env:
79
REGISTRY: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_REGISTRY }}
810
USERNAME: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_USERNAME }}

.github/workflows/build-and-push-bsync-aws.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ on:
33
push:
44
branches:
55
- main
6+
67
env:
78
REGISTRY: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_REGISTRY }}
89
USERNAME: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_USERNAME }}

.github/workflows/build-and-push-bsync-ghcr.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ on:
33
push:
44
branches:
55
- main
6+
67
env:
78
REGISTRY: ghcr.io
89
USERNAME: ${{ github.actor }}

.github/workflows/build-and-push-ozone-aws.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ on:
33
push:
44
branches:
55
- main
6-
- divy/ozone-metadata
6+
77
env:
88
REGISTRY: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_REGISTRY }}
99
USERNAME: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_USERNAME }}

.github/workflows/build-and-push-ozone-ghcr.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ on:
33
push:
44
branches:
55
- main
6+
67
env:
78
REGISTRY: ghcr.io
89
USERNAME: ${{ github.actor }}

0 commit comments

Comments
 (0)