Shared contracts
Some logic runs on the server and in the web app: the server needs it for REST, MCP, the CLI and imports, and the app needs it offline. Each pair is kept in step by one fixture file that a Go test and a TypeScript test both read. Change one side, run both tests; when a fixture is generated, regenerate it with the command in the table, never by hand.
On this page
scripts/contracts.test.mjs (in make repo-check) checks this table: every fixture exists, every file named under Read in Go and Read in TS exists and names its fixture, and every fixture-like file in the repository (*.vectors.json, *_vectors.json, *_fixture.json, *.fixture(s).json) is listed here or below.
| Contract | Go | TypeScript | Fixture | Read in Go | Read in TS | Written by |
|---|---|---|---|---|---|---|
| Op schema (fields per entity) | internal/sync/ops.go |
web/src/lib/ops.ts, types.ts |
internal/sync/testdata/schema_fixture.json |
internal/sync/schema_fixture_test.go |
web/src/lib/schemaFixture.test.ts, web/src/lib/entityTables.test.ts |
hand |
| Client clock and last-writer-wins (the Go property test's model of the client) | internal/sync/convergence_property_test.go (propDevice), sync.Wins |
web/src/lib/hlc.ts |
web/src/lib/hlc.vectors.json |
internal/sync/hlc_vectors_test.go |
web/src/lib/hlc.test.ts |
UPDATE_HLC_VECTORS=1 npx vitest run src/lib/hlc.test.ts |
| Workspaces (partition of synced rows) | internal/sync/partition.go |
web/src/lib/workspaces.ts |
internal/sync/testdata/workspaces_fixture.json |
internal/sync/workspaces_test.go, internal/cli/workspaces_fixture_test.go |
web/src/lib/workspaces.test.ts |
hand |
| End-to-end encryption format | internal/e2ee, internal/e2ee/crypt (CLI) |
web/src/lib/crypto.ts, e2eeEnvelope.ts (also bundled by the Obsidian plugin) |
web/src/lib/e2ee.vectors.json |
internal/e2ee/vectors_test.go, internal/e2ee/crypt/crypt_test.go |
web/src/lib/crypto.test.ts |
go test ./internal/e2ee -run TestVectors -update |
| Device relay sealing | internal/relay |
web/src/lib/relay.ts |
web/src/lib/relay.vectors.json |
internal/relay/vectors_test.go |
web/src/lib/relay.test.ts |
go test ./internal/relay -update |
| Recurrence | internal/recurrence |
web/src/lib/recurrence.ts |
web/src/lib/recurrence.vectors.json |
internal/recurrence/recurrence_test.go |
web/src/lib/recurrence.test.ts |
hand |
| Projected occurrences and exceptions | internal/recurrence/occurrences.go |
web/src/lib/occurrences.ts |
web/src/lib/occurrences.vectors.json |
internal/recurrence/occurrences_test.go |
web/src/lib/occurrences.test.ts |
hand |
| Acting on one occurrence (records, exceptions, repair) and splitting a series (lineage bounds, counts, This and following) | internal/sync/occurrences.go, lineage.go, split.go |
web/src/lib/recurring.ts, lineage.ts, seriesSplit.ts |
web/src/lib/series.fixture.json |
internal/sync/series_fixture_test.go |
web/src/lib/series.fixture.test.ts |
hand |
| Time-zone-fixed due values | internal/zoned |
web/src/lib/dates.ts |
web/src/lib/zoned.vectors.json |
internal/zoned/zoned_test.go |
web/src/lib/dates.test.ts |
hand |
| Inherited custom fields | internal/sync/fields.go |
web/src/lib/fieldTree.ts |
web/src/lib/fields.vectors.json |
internal/sync/fields_vectors_test.go |
web/src/lib/fields.test.ts |
hand |
| Inbox membership | internal/inbox/inbox.go |
web/src/lib/inbox.ts |
web/src/lib/inbox.vectors.json |
internal/inbox/inbox_test.go |
web/src/lib/inbox.test.ts |
hand |
| Time sectors (validity, the viewer's periods, relative names, carry-over, planning list groups) | internal/sector |
web/src/lib/sectors.ts |
web/src/lib/sectors.vectors.json |
internal/sector/sector_test.go |
web/src/lib/sectors.test.ts |
hand |
| Default reminders | internal/defaultreminder |
web/src/lib/defaultReminder.ts |
web/src/lib/defaultReminder.vectors.json |
internal/defaultreminder/defaultreminder_test.go |
web/src/lib/defaultReminder.test.ts |
hand |
| Rotating assignees (next person, valid lists) | internal/sync/rotation.go |
web/src/lib/rotation.ts |
web/src/lib/rotation.vectors.json |
internal/sync/rotation_test.go |
web/src/lib/rotation.test.ts |
hand |
| Built-in view rules | internal/inbox/views.go |
web/src/lib/viewRules.ts |
web/src/lib/views.vectors.json |
internal/inbox/views_test.go |
web/src/lib/viewRules.test.ts |
hand |
| Theme documents (format 1: accepted and refused documents, refusal codes) | internal/themes |
web/src/lib/theme/format.ts |
internal/themes/testdata/ (expected.json) |
internal/themes/themes_test.go |
web/src/lib/theme/format.test.ts, web/src/lib/theme/schema.test.ts |
hand |
| Theme templates (the starter file with notes: what the server accepts of it, as written and with every commented line set) | internal/themes |
web/src/lib/theme/template.ts |
internal/themes/testdata/template/ |
internal/themes/themes_test.go |
web/src/lib/theme/template.test.ts |
UPDATE_THEME_TEMPLATES=1 npx vitest run src/lib/theme/template.test.ts |
| Built-in theme ids and kinds (the server's check of a default theme) | internal/themes/builtin.go |
web/src/lib/theme/builtin/index.ts |
web/src/lib/theme/builtin/ |
internal/themes/builtin_test.go |
web/src/lib/theme/catalogue.test.ts |
hand |
| Filter query parser | internal/filter |
web/src/lib/filter/parse.ts |
docs/filters.md (its examples) |
internal/filter/filter_test.go |
web/src/lib/filter/filter.test.ts |
hand |
| Obsidian Tasks lines (the vault's line grammar and its line-only checks) | internal/vault/line.go, task.go (LineFields) |
obsidian/src/vault/line.ts |
internal/vault/testdata/lines.vectors.json |
internal/vault/linevectors_test.go |
obsidian/src/vault/line.test.ts |
UPDATE_LINE_VECTORS=1 go test ./internal/vault -run TestLineVectors |
| Quick-add parser | internal/quickadd |
web/src/lib/nlp |
web/src/lib/nlp/quickadd.vectors.json |
internal/quickadd/vectors_test.go |
web/src/lib/nlp/vectors.test.ts |
hand |
| Quick-add vocabulary | internal/quickadd/vocab.go |
web/src/lib/nlp |
internal/quickadd/vocab.json |
internal/quickadd/vocab.go (embedded) |
web/src/lib/nlp/vocab.test.ts |
cd web && npm run nlp:vocab |
| Importer date vocabulary | internal/importer/todoistdate.go |
web/src/lib/nlp |
internal/importer/nlpvocab.json |
internal/importer/todoistdate.go (embedded) |
web/src/lib/nlp/vocabExport.test.ts |
UPDATE_NLP_VOCAB=1 npx vitest run src/lib/nlp/vocabExport.test.ts |
| Task origin (recognition, labels, Obsidian note links) | internal/origin |
web/src/lib/taskOrigin.ts |
internal/origin/testdata/origin_vectors.json |
internal/origin/vectors_test.go, internal/origin/obsidian_test.go |
web/src/lib/taskOrigin.test.ts, web/src/lib/shellBridge.fixture.test.ts |
hand |
| Note search (matching, ranking, snippets) | internal/notesearch |
web/src/lib/noteSearch.ts |
internal/notesearch/testdata/vectors.json |
internal/notesearch/notesearch_test.go |
web/src/lib/noteSearch.test.ts |
hand |
| Reusable lists (title normalization, the matched item, duplicate losers) | internal/reuse |
web/src/lib/reusable.ts |
web/src/lib/reusable.vectors.json |
internal/reuse/reuse_test.go |
web/src/lib/reusable.test.ts |
hand |
| Tracked time: the one-running rule | internal/timetrack |
web/src/lib/time.ts |
internal/timetrack/testdata/rule_fixture.json |
internal/timetrack/rule_test.go |
web/src/lib/time.test.ts |
hand |
| Tracked time: CSV export | internal/timetrack |
web/src/lib/time.ts |
internal/timetrack/testdata/csv_fixture.json |
internal/timetrack/csv_test.go |
web/src/lib/time.test.ts |
go test ./internal/timetrack -update-csv |
| Activity events from ops | internal/activity |
web/src/lib/activity.ts |
internal/activity/testdata/activity_fixture.json |
internal/activity/activity_test.go |
web/src/lib/activity.test.ts |
OPENTODO_UPDATE_FIXTURE=1 go test ./internal/activity |
| Automation rule conditions | internal/ruleval |
web/src/lib/rules/match.ts |
web/src/lib/rules/match.vectors.json |
internal/ruleval/ruleval_test.go |
web/src/lib/rules/match.test.ts |
hand |
| Automation rule planner | internal/rules |
web/src/lib/rules/plan.ts, web/src/lib/rules/run/actions.ts |
web/src/lib/rules/plan.fixture.json |
internal/rules/plan_test.go |
web/src/lib/rules/plan.test.ts, web/src/lib/rules/run/run.test.ts |
hand |
| Routine runs (every step kind, the device runs of encrypted accounts) | internal/rules |
web/src/lib/rules/run/ |
web/src/lib/rules/usecases.json |
internal/rules/usecases_test.go |
web/src/lib/rules/run/usecases.test.ts, web/src/lib/rules/usecases.test.ts |
hand |
| Routine steps (shape, types, the D2 reading, the data steps) | internal/ruleval/steps*.go |
web/src/lib/rules/steps.ts, stepData.ts |
web/src/lib/rules/steps.vectors.json |
internal/ruleval/steps_test.go |
web/src/lib/rules/steps.test.ts |
hand |
| Set today's focus (a routine step) | internal/ruleval/focus.go |
web/src/lib/focusSet.ts |
web/src/lib/focusSet.vectors.json |
internal/ruleval/steps_test.go |
web/src/lib/focusSet.test.ts |
hand |
| Filter evaluation (the server's Find tasks) | internal/filter/evaluate.go |
web/src/lib/filter/evaluate.ts |
web/src/lib/filter/evaluate.vectors.json |
internal/filter/evaluate_test.go |
web/src/lib/filter/evaluate.vectors.test.ts |
hand |
| Weekly digest statistics | internal/digest/stats.go |
web/src/lib/stats.ts |
internal/digest/testdata/stats_fixture.json |
internal/digest/stats_test.go |
web/src/lib/stats.test.ts |
hand |
| Available tasks (project flows) | internal/flow |
web/src/lib/flow.ts |
internal/flow/testdata/flow_fixture.json |
internal/flow/flow_test.go |
web/src/lib/flow.test.ts |
hand |
| Calendar frame occurrences | internal/frames |
web/src/lib/frames.ts |
web/src/lib/frames.vectors.json |
internal/frames/frames_test.go |
web/src/lib/frames.test.ts |
hand |
| Today's closed list (slot fill) | internal/closedlist |
web/src/lib/closedList.ts |
web/src/lib/closedList.vectors.json |
internal/closedlist/closedlist_test.go |
web/src/lib/closedList.test.ts |
hand |
| Next action (method views) | internal/digest |
web/src/lib/method/rules.ts |
internal/digest/testdata/next_action_fixture.json |
internal/digest/nextaction_test.go |
web/src/lib/method/rules.test.ts |
hand |
| On-device assistant tools | internal/assistant |
web/src/lib/assistantDevice |
web/src/lib/assistantDevice/manifest.json |
internal/assistant/manifest_test.go |
web/src/lib/assistantDevice/manifest.ts |
go test ./internal/assistant -run Device -update |
| On-device assistant planners | internal/assistant, internal/agenttools |
web/src/lib/assistantDevice/plan.ts |
web/src/lib/assistantDevice/plan.vectors.json |
internal/assistant/manifest_test.go |
web/src/lib/assistantDevice/tools.test.ts |
go test ./internal/assistant -run Device -update |
| On-device assistant prompt | internal/assistant |
web/src/lib/assistantDevice/prompt.ts |
web/src/lib/assistantDevice/prompt.vectors.json |
internal/assistant/manifest_test.go |
web/src/lib/assistantDevice/loop.test.ts |
go test ./internal/assistant -run Device -update |
| Describe it on the device (routines-describe-it) | internal/assistant (routinedraft.go) |
web/src/lib/rules/describeDevice.ts |
web/src/lib/assistantDevice/routineDraft.vectors.json |
internal/assistant/routinedraft_test.go |
web/src/lib/rules/describe.test.ts |
go test ./internal/assistant -run Device -update |
| Provider error reading (Chat Completions) | internal/assistant (chatError) |
web/src/lib/assistantDevice/providers.ts (statusError) |
web/src/lib/assistantDevice/errors.vectors.json |
internal/assistant/errors_test.go |
web/src/lib/assistantDevice/providers.test.ts |
go test ./internal/assistant -run Device -update |
Thin vector sets give false confidence: when you change one of these modules, add the case you fixed or added to its fixture first.
Other fixtures are not shared between the two languages: the agent tool lists (internal/agenttools/testdata/*.golden.json, also read by the Claude plugin check), the MCP tool list, the CLI schema, the importers' golden outputs, web/src/lib/habits.vectors.json, web/src/lib/briefing.fixtures.json, web/src/lib/accent.vectors.json (the custom accent derivation, client-only: the server never renders the accent) and web/src/lib/theme/derive.vectors.json (the theme derivation, client-only for the same reason).
The v1 surface snapshot#
Not a shared fixture but the same idea one level up: internal/httpapi/testdata/v1_surface.json records what every 1.x release keeps (stability and compatibility): the /api/v1/ routes with their policy, success statuses, request and response fields (read from the handlers' types), the error codes with their statuses, the op schema with each field's kind, the export's members and version, the MCP tools with their arguments and read results, and the ot commands, flags, exit codes and --json schemas (from internal/cli/testdata/schema.golden.json). go test ./internal/httpapi -run TestV1SurfaceIsAdditive fails when an entry is gone or changed and when a new one is not recorded; -update adds new entries and never removes or changes one. After v1.0.0 is tagged, make repo-check also compares the file with the one at that tag. TestGeneratedOpSchema refuses to regenerate the op schema fixture without a field it had.
Upgrade fixtures#
internal/store/testdata/upgrade/ holds one fixture per released minor version: data.tar.gz, the data directory that version wrote while the fixed scenario of scripts/release/upgrade-scenario.mjs ran against it, and manifest.json, what the version itself reported (counts read through the API, the op count, a device's cursor, the credentials, the source commit). scripts/release/upgrade-fixture.sh <version> [ref] builds the server from the tag (or ref) in a temporary checkout and writes both. go test ./internal/tenant -run TestUpgradeFromEveryRelease opens each fixture with the current server and compares. A fixture is never regenerated once its version is released; make release-prepare refuses a new minor without its own.
Client recordings#
internal/sync/testdata/v1_clients/ holds the calls of the web app's sync client (web.json), ot (ot.json) and the browser extension (extension.json) against the server, in order, with sessions and tokens replaced by labels (session:1, <token:1>) and passwords by <password>. They come from the contract tests (web/src/lib/*.contract.test.ts) run through the recorder in web/src/test/recorder.ts: make server cli && cd web && npm run test:contract:record. go test ./internal/tenant -run TestV1ClientsReplay replays each file against a fresh current server (mapping the recorded ids and tokens to the replay's own) and fails on another status, a missing field or a field of another type. They are recorded once per major version and never regenerated within it; make repo-check checks that they hold no secret.