Skip to content

Fix StudioTeam DI gap and prevent regression#499

Merged
eanzhao merged 1 commit intodevfrom
fix/2026-04-28_studio-team-projection-reader-di-gap
Apr 28, 2026
Merged

Fix StudioTeam DI gap and prevent regression#499
eanzhao merged 1 commit intodevfrom
fix/2026-04-28_studio-team-projection-reader-di-gap

Conversation

@eanzhao
Copy link
Copy Markdown
Contributor

@eanzhao eanzhao commented Apr 28, 2026

Problem

Mainnet host startup fails on dev:

Unable to resolve service for type
'Aevatar.CQRS.Projection.Stores.Abstractions.IProjectionDocumentReader`2[
Aevatar.Studio.Projection.ReadModels.StudioTeamCurrentStateDocument, System.String]'
while attempting to activate
'Aevatar.Studio.Projection.QueryPorts.ProjectionStudioTeamQueryPort'.

ADR-0017 step 6+9 (StudioTeam projection + application service layer) added the
read model, projector, metadata provider, query port, and command port but
missed wiring StudioTeamCurrentStateDocument into
StudioProjectionReadModelServiceCollectionExtensions. With
ValidateOnBuild = true on Aevatar.Mainnet.Host.Api, the container build
fails immediately and the pod can't start.

Unrelated to PR #495 — that merge happened to be the dev tip when the gap
surfaced.

Solution

Fix the DI gap (StudioProjectionReadModelServiceCollectionExtensions)

  • RegisterElasticsearch<StudioTeamCurrentStateDocument>(...)
  • RegisterInMemory<StudioTeamCurrentStateDocument>(...)
  • Extend HasAllStudioDocumentReaders predicate to require both providers.
  • Add StudioTeamState.Descriptor to BuildStudioStateTypeRegistry (ES
    state-event deserialization).

Prevent regression (two layers, both validated)

Static guard
tools/ci/studio_projection_readmodel_registration_guard.sh:
scans every IProjectionReadModel<TDoc> under
src/Aevatar.Studio.Projection/ReadModels/ and requires the hosting file
to call RegisterElasticsearch<TDoc>, RegisterInMemory<TDoc>, and
HasDocumentReaderForProvider<TDoc>. Wired into architecture_guards.sh
(runs in fast-gates). Cheap and immediate.

Composition smoke — new CI job host-composition-smoke:
runs MainnetHostCompositionTests on every PR. The existing test exercises
the full Aevatar.Mainnet.Host.Api container with InMemory providers and
would have caught this gap — but the test only ran in slow-test-guards /
split-test-guards, both gated to schedule / workflow_dispatch.
Adding a dedicated job ensures every PR validates DI composition.

The two layers are intentional: the guard catches the specific Studio
projection registration pattern at lint speed; the composition smoke catches
the broader class of "any singleton's transitive dep is missing".

Impact Paths

  • src/Aevatar.Studio.Hosting/StudioProjectionReadModelServiceCollectionExtensions.cs
  • tools/ci/studio_projection_readmodel_registration_guard.sh (new)
  • tools/ci/host_composition_smoke.sh (new)
  • tools/ci/architecture_guards.sh
  • .github/workflows/ci.yml

Validation

  • bash tools/ci/studio_projection_readmodel_registration_guard.sh
    • FAILS against pre-fix state with the exact missing-call list.
    • PASSES against fixed state.
  • bash tools/ci/host_composition_smoke.sh
    • FAILS against pre-fix state with the ValidateService stack trace
      matching prod.
    • PASSES against fixed state (3 tests in MainnetHostCompositionTests,
      all green).
  • dotnet build src/Aevatar.Studio.Hosting/Aevatar.Studio.Hosting.csproj
    succeeds.

Test plan

  • CI green on this PR (architecture_guards + new host-composition-smoke).
  • Deploy to staging, confirm pod starts past ValidateOnBuild.
  • Verify /api/studio/teams/... endpoints respond (proves the registered
    reader actually serves queries).

🤖 Generated with Claude Code

Mainnet host startup fails ValidateOnBuild because
ProjectionStudioTeamQueryPort needs IProjectionDocumentReader<
StudioTeamCurrentStateDocument, string>, which was never registered:
ADR-0017 step 6+9 added the projector, document, metadata provider, and
query port but missed wiring the ES/InMemory store in
StudioProjectionReadModelServiceCollectionExtensions.

Fix:
- Register StudioTeamCurrentStateDocument with both ES and InMemory
  document stores; add to HasAllStudioDocumentReaders predicate.
- Add StudioTeamState.Descriptor to the state TypeRegistry used by ES
  state-event deserialization.

Prevent regression:
- New CI guard tools/ci/studio_projection_readmodel_registration_guard.sh
  scans every IProjectionReadModel<TDoc> in Studio.Projection ReadModels
  and requires the hosting file to call RegisterElasticsearch<TDoc>,
  RegisterInMemory<TDoc>, and HasDocumentReaderForProvider<TDoc>. Wired
  into architecture_guards.sh (runs in fast-gates).
- New CI job host-composition-smoke runs MainnetHostCompositionTests on
  every PR. The existing test exercises the full Mainnet container with
  ValidateOnBuild and would have caught this gap, but the test only ran
  in slow-test-guards / split-test-guards which are gated to scheduled /
  workflow_dispatch -- never on PRs.

Validation:
- Guard FAILS against pre-fix state with the exact missing-call list,
  PASSES against fixed state.
- MainnetHostCompositionTests FAILS against pre-fix state with the
  ValidateService stack trace prod hit, PASSES against fixed state.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@eanzhao
Copy link
Copy Markdown
Contributor Author

eanzhao commented Apr 28, 2026

Review Summary

LGTM — PR质量很好,可以合并。

修复正确性

  • DI注册补全正确:RegisterElasticsearchRegisterInMemoryHasDocumentReaderForProviderTypeRegistry 四处都补齐了
  • 与现有 StudioMemberCurrentStateDocument 的注册模式完全一致

防护层设计

  • 静态guard (studio_projection_readmodel_registration_guard.sh):rg提取 IProjectionReadModel<TDoc> → 校验hosting文件三处调用,逻辑清晰,误报风险低
  • 组合smoke (host_composition_smoke):从 slow-test-guards 提升到每个PR运行,合理——这是唯一能捕获传递依赖缺失的手段

CI

  • 所有required checks已通过(fast-gates、host-composition-smoke等)
  • host-composition-smoke job条件正确:排除schedule,覆盖PR/push/main/dev

一个小建议(非阻塞)

guard脚本的 rg pattern \bIProjectionReadModel<\s*(\w+)\s*> 只匹配单泛型参数。如果未来 IProjectionReadModel 扩展为多参数接口会漏检,但当前场景足够。

@eanzhao eanzhao merged commit df01a62 into dev Apr 28, 2026
12 checks passed
@eanzhao
Copy link
Copy Markdown
Contributor Author

eanzhao commented Apr 28, 2026

Review: Guard 不完整 — 缺少 BuildStudioStateTypeRegistry 检查

DI 注册修复本身是完整的(4 处全部补齐),PR 的核心问题已正确解决。

studio_projection_readmodel_registration_guard.sh 存在一个覆盖缺口:

问题

Guard 只验证了 3 个注册点:

  1. RegisterElasticsearch<TDoc>
  2. RegisterInMemory<TDoc>
  3. HasDocumentReaderForProvider<TDoc>

但漏掉了第 4 个:BuildStudioStateTypeRegistry() 中的 State.Descriptor 注册

这次 PR 的 fix 恰好同时补了 StudioTeamState.Descriptor,但如果未来有人新增 TDoc 时只注册了前 3 个而漏掉 Descriptor,guard 会 pass,而 ES 投影的 state-event 反序列化会在运行时静默失败——这正是 guard 声称要防止的那类"module tests pass, runtime breaks"场景。

建议

在 guard 的 for pattern 循环中增加一个检查。由于 Descriptor 的命名是 {TDoc去掉CurrentStateDocument后缀}State.Descriptor,可以用 sed 做转换:

# 在 for pattern 循环之后,增加 Descriptor 检查
state_name="$(echo "${doc}" | sed 's/CurrentStateDocument$//')State"
if ! rg -q --fixed-strings "${state_name}.Descriptor" "${hosting_file}"; then
  missing+="  ${doc}: ${state_name}.Descriptor missing in BuildStudioStateTypeRegistry"$'\n'
fi

或者如果命名规则不够稳定,至少在 guard 注释的 "must be wired through all three" 里明确标注还有第四处(BuildStudioStateTypeRegistry),让后续维护者知道。

其余部分 LGTM

  • DI 修复完整、模式一致
  • host_composition_smoke.sh + CI job 作为第二层防护思路正确
  • CI condition 合理(排除 schedule,覆盖 PR/push to main/dev)

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 28, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 71.52%. Comparing base (14e39ae) to head (74cf6e5).
⚠️ Report is 2 commits behind head on dev.

@@            Coverage Diff             @@
##              dev     #499      +/-   ##
==========================================
+ Coverage   71.06%   71.52%   +0.45%     
==========================================
  Files        1235     1235              
  Lines       89463    89467       +4     
  Branches    11705    11705              
==========================================
+ Hits        63575    63989     +414     
+ Misses      21331    20895     -436     
- Partials     4557     4583      +26     
Flag Coverage Δ
ci 71.52% <100.00%> (+0.45%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...oProjectionReadModelServiceCollectionExtensions.cs 91.79% <100.00%> (+0.25%) ⬆️

... and 35 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown
Contributor Author

@eanzhao eanzhao left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall the fix is correct and the defense-in-depth approach (static guard + CI smoke) is well designed. A few observations:

1. host_composition_smoke.sh — silent pass risk

If the test class MainnetHostCompositionTests is ever renamed or the FQN changes, the --filter will silently match zero tests and dotnet test returns exit code 0. This turns the smoke into a no-op CI check. Consider adding an explicit count guard:

output=$(dotnet test ... --filter "FullyQualifiedName~MainnetHostCompositionTests" 2>&1)
if ! echo "$output" | grep -q "Total tests:"; then
  echo "No tests matched — MainnetHostCompositionTests may have been renamed"
  exit 1
fi

Or alternatively, filter at the test-project level and validate the test count result.

2. Guard script regex robustness (minor)

The regex \bIProjectionReadModel<\s*(\w+)\s*> works for all current read models, but \w+ won't match generic-parameterized types (e.g. IProjectionReadModel<Foo<T>>). If a read model ever takes a type parameter, the guard will silently miss it. Consider using [\w<>]+ or a more permissive capture group. Not blocking, just a forward-looking note.

3. Everything else LGTM

  • DI registrations (Elasticsearch + InMemory) match the existing pattern exactly
  • HasAllStudioDocumentReaders is consistent with the registration blocks
  • StudioTeamState.Descriptor in BuildStudioStateTypeRegistry follows the same pattern as StudioMemberState.Descriptor
  • CI integration is clean and properly gated to PR/push events only
  • Branch naming follows the repo convention (fix/YYYY-MM-DD_purpose)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant