Skip to main content

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.tsapps/api/src/data-sync/data-sync.e2e.spec.ts
  • api/data-sync.service.spec.tsapps/api/src/data-sync/data-sync.service.spec.ts
  • api/data-sync-dispatcher.service.spec.tsapps/api/src/data-sync/data-sync-dispatcher.service.spec.ts
  • api/data-sync.controller.spec.tsapps/api/src/data-sync/data-sync.controller.spec.ts
  • api/github-app-sync.service.spec.tsapps/api/src/integrations/github-app/github-app-sync.service.spec.ts
  • api/work-repository.spec.tspackages/agent/src/database/repositories/work.repository.spec.ts
  • api/markdown-generator.service.spec.tspackages/agent/src/generators/markdown-generator/markdown-generator.service.spec.ts
  • api/markdown-repository.spec.tspackages/agent/src/generators/markdown-generator/markdown-repository.spec.ts
  • api/activity-feed.service.spec.tsapps/api/src/works/activity-feed/__tests__/activity-feed.service.spec.ts
  • api/data-sync-metrics.spec.tspackages/agent/src/cache/data-sync-metrics.spec.ts
  • api/data-sync-feature-flags.spec.tspackages/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.tspackages/tasks/src/__tests__/data-repo-sync-dispatcher.task.spec.ts
  • web/sync-event-row.spec.tsxapps/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

CategoryCount
Automated unit / integration15 of 16 ACs (AC-1AC-8, AC-10AC-16)
Inherited from upstream service specAC-4 (lock TTL recovery), AC-9 (webhook signature)
Manual / soakNone — every AC has at least one automated assertion

Test-suite totals at G10

SuiteTests
apps/api/src/data-sync/data-sync.service.spec.ts21
apps/api/src/data-sync/data-sync.e2e.spec.ts8
apps/api/src/data-sync/data-sync.controller.spec.ts7
apps/api/src/data-sync/data-sync-dispatcher.service.spec.ts8
apps/api/src/integrations/github-app/github-app-sync.service.spec.ts13
packages/agent/src/database/repositories/work.repository.spec.ts5
packages/agent/src/cache/data-sync-metrics.spec.ts15
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.ts10
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 assertions110+

Known follow-ups (out of EW-628 scope; tracked for Phase 10)

  • AC-2 no-changes skipped-row rate-limiting via the noise-window cache — currently the dispatcher relies on the underlying syncFromDataRepo being idempotent. A user-visible skipped { reason: 'no-changes' } row would require a pre-check (e.g. gitFacade.getLatestCommit compared against lastSyncedDataRepoSha BEFORE 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.ts is the authoritative pre-soak signal until those fixtures land.