EW-628 — Acceptance-criteria → test coverage matrix
Feature ID: data-repo-instant-sync
Acceptance spec: ./acceptance.md
Implementation PR: ever-works/ever-works#799
Status: G12 — PR #799 ready for review. Coverage snapshot finalised at end of implementation phase.
The 16 acceptance criteria in acceptance.md are pinned to one or more test locations below. Every AC either has automated coverage in the branch, or carries an explicit manual row pointing at the operator-driven verification step.
Test-file shorthand:
api/data-sync.e2e.spec.ts→apps/api/src/data-sync/data-sync.e2e.spec.tsapi/data-sync.service.spec.ts→apps/api/src/data-sync/data-sync.service.spec.tsapi/data-sync-dispatcher.service.spec.ts→apps/api/src/data-sync/data-sync-dispatcher.service.spec.tsapi/data-sync.controller.spec.ts→apps/api/src/data-sync/data-sync.controller.spec.tsapi/github-app-sync.service.spec.ts→apps/api/src/integrations/github-app/github-app-sync.service.spec.tsapi/work-repository.spec.ts→packages/agent/src/database/repositories/work.repository.spec.tsapi/markdown-generator.service.spec.ts→packages/agent/src/generators/markdown-generator/markdown-generator.service.spec.tsapi/markdown-repository.spec.ts→packages/agent/src/generators/markdown-generator/markdown-repository.spec.tsapi/activity-feed.service.spec.ts→apps/api/src/works/activity-feed/__tests__/activity-feed.service.spec.tsapi/data-sync-metrics.spec.ts→packages/agent/src/cache/data-sync-metrics.spec.tsapi/data-sync-feature-flags.spec.ts→packages/agent/src/cache/data-sync-metrics.spec.ts(flag accessor block) +apps/api/src/data-sync/data-sync-dispatcher.service.spec.ts(flag-gate block)api/dispatcher-task.spec.ts→packages/tasks/src/__tests__/data-repo-sync-dispatcher.task.spec.tsweb/sync-event-row.spec.tsx→apps/web/src/components/works/detail/activity/SyncEventRow.unit.spec.tsx
| AC | Title | Test location(s) | Notes |
| ----- | ---------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------- | ---------------- | ------------------------------------------------------------------------- |
| AC-1 | Webhook-driven sync (App installed) | api/data-sync.e2e.spec.ts (AC-1 — webhook flush dispatched via the cron), api/data-sync-dispatcher.service.spec.ts (Path A fan-out), api/github-app-sync.service.spec.ts (updates pendingSyncRequestedAt on every matching Work when the flag is on), api/markdown-generator.service.spec.ts (G5: returns beforeSha = work.lastSyncedDataRepoSha and afterSha = provider HEAD) | Cron debounce semantics (≤ 30 s coalesce) verified at the dispatcher SQL level via the pendingSyncRequestedAt <= now - debounceMs filter in WorkRepository.findWebhookFlushDueWorks (api/work-repository.spec.ts). Browser-level fixme remains in apps/web/e2e/data-sync.spec.ts until a mock GitHub App lands. |
| AC-2 | Poller-driven sync (App not installed) | api/data-sync.e2e.spec.ts (AC-2 — poller flush dispatched via the cron), api/data-sync-dispatcher.service.spec.ts (Path B fan-out + lastPolledAt stamping), api/work-repository.spec.ts (poller-due query) | Noise-window rate-limit for no-changes is currently a TODO at the dispatcher level (the underlying syncFromDataRepo is idempotent — empty diff → no push — so the user-visible behaviour is identical pending the explicit skipped:no-changes row). Flagged in PR body as a Phase-10 follow-up. |
| AC-3 | Mutex with generation pipeline | api/data-sync.service.spec.ts (Gate 2 generation-in-progress + noise-window rate-limit), api/data-sync.e2e.spec.ts (AC-3 — mutex with the generation pipeline 2 scenarios), api/data-sync.service.spec.ts (lock contention → skipped:sync-in-progress for all 3 SyncSource values) | The isLocked peek for the symmetric mutex against the generation dispatcher is covered by 5 G2 assertions in data-sync.service.spec.ts. |
| AC-4 | Lock TTL recovery | packages/agent/src/cache/distributed-task-lock.service.spec.ts (pre-existing — covers stale-lock cleanup at the tryAcquire SQL level) | DataSyncService uses runExclusive with ttlMs = config.subscriptions.dataSync.getLockTtlSeconds() * 1000 (300 s default). Stale-lock cleanup is a property of DistributedTaskLockService, not the EW-628 caller — the existing service spec is authoritative. |
| AC-5 | Render parity with full pipeline | api/markdown-generator.service.spec.ts (syncFromDataRepo — EW-628 render-only entry 6 scenarios), api/markdown-repository.spec.ts (write-count contract) | syncFromDataRepo calls this.initialize(work, user, ...) — the same render path the full pipeline uses minus the AI items step. AC-5's byte-identical README assertion is structural (delegated to existing initialize tests); the EW-628 entry just records filesChanged and the SHA. |
| AC-6 | Idempotency | api/data-sync.service.spec.ts (Gate 3 success — the success path always emits a row, even when filesChanged === 0) | The render gate runs unconditionally; idempotency is enforced by git's diff (no commit → no push) plus the activity row that records filesChanged: 0 so users still see the attempt. |
| AC-7 | Data repo unreachable | api/data-sync.e2e.spec.ts (AC-7 — data repo unreachable), api/data-sync.service.spec.ts (Gate 3 failed (404)), api/data-sync.service.spec.ts (telemetry counters → failed emits data_sync_failed_total) | Verifies errorClass = 'data-repo-unreachable', errorTail populated, lastSyncedDataRepoSha unchanged, retry-after cache key set. |
| AC-8 | Main repo push rejected | api/data-sync.service.spec.ts (Gate 3 failed: maps push-rejected errors to errorClass main-repo-push-rejected), api/data-sync.e2e.spec.ts (force-sync failed envelope with errorClass: 'main-repo-push-rejected') | classifyError returns main-repo-push-rejected for /reject | non-fast-forward | protected branch | merge conflict/i. Retry semantics inherit from AC-7's retry-after cache. |
| AC-9 | Webhook signature invalid | apps/api/src/integrations/github-app/__tests__/github-app-webhook.controller.spec.ts (pre-existing — signature check is owned by the controller, not EW-628 code) | EW-628 G6 (handlePushEvent) is reached only AFTER signature verification passes; an invalid signature short-circuits at the controller before any sync logic runs. |
| AC-10 | POST /api/works/:id/sync | api/data-sync.controller.spec.ts (7 envelope shape assertions), api/data-sync.e2e.spec.ts (AC-10 — POST /api/works/:id/sync 3 scenarios — enqueued / skipped / failed) | The auth shape is enforced upstream by the global JWT guard; controller specs use a stub user. |
| AC-11 | Sync chip + rows | web/sync-event-row.spec.tsx (Phase 7 — SyncEventRow rendering for all 3 variants), api/activity-feed.service.spec.ts (EW-628 sync category queries the three DATA_SYNC_* action types in a single IN query) | UI chip wiring ('sync' in FEED_CATEGORIES) lives in apps/web/src/lib/api/works/activity-feed.types.ts. 20-locale translation keys land in apps/web/messages/*.json. |
| AC-12 | Counters and histogram | api/data-sync.service.spec.ts (telemetry counters (EW-628 G10) — 5 assertions: lockContention / success+durationMs / skipped / failed / resilience), api/data-sync-metrics.spec.ts (Phase 8 — name contract) | errorTail deliberately NOT included in the failed counter labels (PII / high-cardinality stderr). Verified by the failed-counter assertion that explicitly checks not.toHaveProperty('errorTail'). |
| AC-13 | Both flags off (default at first deploy) | api/data-sync-feature-flags.spec.ts, api/data-sync-dispatcher.service.spec.ts (flag gate → returns an empty summary and runs neither path when dispatcherEnabled = false), api/github-app-sync.service.spec.ts (flag-gated: skips the lookup + UPDATE when subscriptions.dataSync.webhookEnabled is false) | Default values pinned in apps/api/src/config/... and the Phase 8 spec. Webhook handler returns silently when flag is off; dispatcher returns an empty summary. |
| AC-14 | No regression to the scheduled pipeline | api/markdown-generator.service.spec.ts (existing 90+ initialize() scenarios still pass — confirmed in the G5 commit), apps/api/test:works/.../scheduled-updates.spec.ts (pre-existing) | G5 changed initialize() to return { filesChanged } instead of void; all existing callers ignore the return value (backwards compatible). |
| AC-15 | Migration safety | apps/api/src/migrations/1779100000000-AddDataRepoInstantSync.ts + reversible down migration (Phase 1 commit 96b2b0b1) | bigint-ms @TimestampColumn semantics; no data backfill (githubAppInstalled flips lazily via the App's installation_repositories webhook). |
| AC-16 | Telemetry sampling sanity | api/data-sync.service.spec.ts (telemetry failures never break the sync path) | The recordMetric helper swallows AnalyticsService.track throws; verified by stubbing track to throw and asserting the sync still returns status: 'success'. |
Tally
| Category | Count |
|---|---|
| Automated unit / integration | 15 of 16 ACs (AC-1–AC-8, AC-10–AC-16) |
| Inherited from upstream service spec | AC-4 (lock TTL recovery), AC-9 (webhook signature) |
| Manual / soak | None — every AC has at least one automated assertion |
Test-suite totals at G10
| Suite | Tests |
|---|---|
apps/api/src/data-sync/data-sync.service.spec.ts | 21 |
apps/api/src/data-sync/data-sync.e2e.spec.ts | 8 |
apps/api/src/data-sync/data-sync.controller.spec.ts | 7 |
apps/api/src/data-sync/data-sync-dispatcher.service.spec.ts | 8 |
apps/api/src/integrations/github-app/github-app-sync.service.spec.ts | 13 |
packages/agent/src/database/repositories/work.repository.spec.ts | 5 |
packages/agent/src/cache/data-sync-metrics.spec.ts | 15 |
packages/agent/src/generators/markdown-generator/markdown-generator.service.spec.ts (EW-628 block) | 6 |
packages/agent/src/generators/markdown-generator/markdown-repository.spec.ts (G5 block) | 4 |
packages/tasks/src/__tests__/data-repo-sync-dispatcher.task.spec.ts | 10 |
apps/api/src/works/activity-feed/__tests__/activity-feed.service.spec.ts (G9 block) | 1 |
apps/web/src/components/works/detail/activity/SyncEventRow.unit.spec.tsx | (Phase 7 — 12) |
| Total EW-628-attributed assertions | 110+ |
Known follow-ups (out of EW-628 scope; tracked for Phase 10)
- AC-2
no-changesskipped-row rate-limiting via the noise-window cache — currently the dispatcher relies on the underlyingsyncFromDataRepobeing idempotent. A user-visibleskipped { reason: 'no-changes' }row would require a pre-check (e.g.gitFacade.getLatestCommitcompared againstlastSyncedDataRepoShaBEFORE the lock acquire). Cheap and additive — lands once the dispatcher SQL eligibility query has telemetry showing the actual ratio. - Full browser-level Playwright run for AC-1 / AC-2 / AC-3 / AC-7 — fixtures (mock GitHub App, Redis-backed lock, fake markdown-generator lifecycle) gated behind a separate ticket. In-process integration coverage in
data-sync.e2e.spec.tsis the authoritative pre-soak signal until those fixtures land.