Follow-up to the 0c693237 'sdk version bump' (net10.0 -> net11.0
across the whole solution) and the work to get PostIt.Android building
under the .NET 11 preview SDK installed locally (~/.dotnet):
- Drop Xamarin.AndroidX.Browser and Xamarin.AndroidX.Core.SplashScreen
from PostIt/Directory.Packages.props. They are not pulled by
Avalonia.Android 12.1.1 (its nuspec declares AppCompat + Window only),
are not referenced by any PostIt code, and we already removed the
SplashScreen XML attributes from styles.xml in commit 6b2867c8.
- PostIt.Android.csproj: bump SupportedOSPlatformVersion 23.0.0 ->
24.0.0. Required by the Microsoft.Android.Sdk.Linux 37.0.0-preview.7
target (Android API 24 is the new minimum under .NET 11); below that
the build emits NETSDK warnings about an EoL target SDK level.
Build status (PostIt.Android, Debug, android-x64, EmbedAssemblies):
0 errors, 15 warnings, ~4m24s cold. The locally-installed .NET 11
SDK 11.0.100-preview.7.26381.103 needs /opt/android-sdk/platforms
/android-37.0 to exist; Paul has set up a symlink to the
android-37.1 (Android 17 preview) install until Microsoft catches
up the SDK manifest.
PostIt.Tests: 49/62 passing after the migration; 13 new failures,
most of them HttpListener teardown noise around the OIDC stub
authority suite. Out of scope of this commit.
WIP: addressing the layer-3 WindowingPlatformStub.NotSupportedException
crash on PostIt.Android by downgrading to the Avalonia 11 / Avalonia.Maui
11 stack, where the Android runtime platform layer (IWindowingPlatform)
gets properly bound via the MAUI embedding extension instead of being
left as a stub.
Touched files:
- src/PostIt/Directory.Packages.props:
- Avalonia 12.1.1 -> 11.3.20 (Avalonia, Avalonia.Android,
Avalonia.AvaloniaEdit, Avalonia.Browser, Avalonia.Desktop,
Avalonia.Fonts.Inter, Avalonia.Themes.Fluent).
- New: Avalonia.Maui 11.3.0.
- Microsoft.Maui.Essentials 10.0.100 -> 8.0.100 (aligns with the
Microsoft.Maui.Controls 8.0.x family that Avalonia.Maui 11.3.0
pulls transitively).
- New: Microsoft.Maui.Controls 8.0.100.
- src/PostIt/PostIt.Android/PostIt.Android.csproj:
- TargetFramework net10.0-android -> net9.0-android36.0 (Avalonia
Android 11.3.20 targets net8.0-android34.0, but the
compat-ascending fallback lets us consume from net9.0-android36.0
against the API-36 platform that's already installed locally).
- Add Avalonia.Maui PackageReference.
- Restore Microsoft.Maui.Essentials PackageReference (left in
earlier removal during the MAUI-cause-of-crash hypothesis check).
- src/PostIt/PostIt/PostIt.csproj: TargetFramework net10.0 -> net9.0
(must match what PostIt.Android consumes transitively, otherwise
the build chain breaks).
- src/Yavsc.Abstract/Yavsc.Abstract.csproj: net10.0 -> net9.0 (same
reason; six other consumers stay on net10.0 and pick up the new
assembly via standard TFM compat).
- src/Yavsc.Api.Client/Yavsc.Api.Client.csproj: net10.0 -> net9.0
(same reason).
- src/PostIt/PostIt.Android/Application.cs: rewritten as a bare
Android.App.Application. Avalonia 11 no longer ships
AvaloniaAndroidApplication<TApp> (that was an Avalonia 12
introduction); the Avalonia init now lives in MainActivity via
AvaloniaMainActivity<TApp>. The class is kept only so the
[Application] manifest entry remains.
- src/PostIt/PostIt.Android/MainActivity.cs: now inherits
AvaloniaMainActivity<App> and overrides CustomizeAppBuilder to
chain .UseMaui<PostIt.Maui.MauiEmbeddingApp>(this) before
.WithInterFont(). AvaloniaMainActivity<TApp> handles the
AvaloniaView initialisation in OnCreate.
- src/PostIt/PostIt/Maui/MauiEmbeddingApp.cs (new): empty
Microsoft.Maui.Controls.Application used as the embedding host for
Avalonia.Maui. The MAUI window it produces is consumed by the
Avalonia.Maui embedding pipeline and never surfaces to the user;
Avalonia owns the actual visual tree.
Build status: 11 errors remaining, all CS0234 / CS0246 against
'Avalonia.Controls.ContentPage'. Avalonia 11 did not ship
ContentPage (it's an Avalonia 12 type, presumably for MAUI parity).
All seven PostIt pages inherit ContentPage today and need to be
reworked to UserControl before the build can complete. Follow-up
commit will do that.
Reference: AGENTS.md, 'PostIt.Android boot crash sur AVD x86_64 :
investigation par couches', couche 3.
- PostIt.Android.csproj: drop Xamarin.AndroidX.Core.SplashScreen and
Xamarin.AndroidX.Browser. Neither is pulled transitively by
Avalonia.Android 12.1.1 (its nuspec declares only AppCompat +
Window), and they are not referenced anywhere in PostIt code.
- Resources/values-v31/styles.xml: drop the windowSplashScreen*
items and the 'postSplashScreenTheme' reference, which require
the dropped SplashScreen package. We fall back to the system
splash screen on Android 12+ devices; a custom one can come
back when we add it on purpose.
Two adjustments after the Android boot investigation
(cf. AGENTS.md section 'PostIt.Android boot crash sur AVD
x86_64 : investigation par couches'):
- Avalonia 12.0.4 -> 12.1.1 in PostIt/Directory.Packages.props.
12.1.x is the current Avalonia 12 stable line; 12.0.4 had a
known WindowingPlatformStub regression on Pixel x86_64 AVDs
(issue AvaloniaUI/Avalonia#18459 family). 12.1.1 does not
actually fix the layer-3 NotSupportedException on this
specific Pixel + Android 16 + x86_64 combination (verified
by rebuild + logcat capture on 2026-08-22), but staying on
12.0.4 forever is wrong, and 12.1.1 is the baseline most
users are on. The Makefile qemu-build already passes
-p:EmbedAssembliesIntoApk=true and -p:RuntimeIdentifier=
android-x64 to work around the Fast Deployment / AOT issues.
- qemu-logcat-boot LOGCAT_BOOT_WAIT 15s -> 30s. The crash
reproduces well under 5s, so the wait value is moot for
diagnosis, but 30s gives a wider margin when the runtime
happens to be slow to JIT (boot tries to draw a frame in
texture-pass scenarios). Override on the command line is
unchanged: 'make qemu-logcat-boot LOGCAT_BOOT_WAIT=N'.
No MAUI change: Microsoft.Maui.Essentials was deliberately
unhooked during a hypothesis check and put back to keep the
contact-flow code path stable. The MAUI removal made no
difference to the crash (verified).
The qemu install path used to crash on startup with
'No assemblies found in files/.__override__/<rid>':
monodroid-glue.cc:757 / SIGABRT. Root cause: the .NET 10 Android
SDK defaults to Fast Deployment in Debug, which ships the APK
without managed assemblies and pushes them at runtime via adb —
not viable on the qemu emulator.
Fix:
- Replace the no-op -p:AndroidEnableFastDeployment=false flag
(does not exist as an MSBuild property in the .NET 10 SDK) with
-p:EmbedAssembliesIntoApk=true, which forces the build to
cross-compile the managed assemblies into native lib_*.dll.so
libraries for every ABI and pack them into the APK under
lib/<arch>/. The Mono runtime then loads them directly,
bypassing the Fast Deployment code path entirely.
- Add CONFIG variable passthrough so 'make qemu-install CONFIG=Release'
builds an optimised APK for release smoke tests.
- qemu-build now consumes $(CONFIG) instead of hardcoded 'Debug'
for the APK output path.
Side effect: the Debug APK balloons from ~13 MB (libs only) to
~160 MB (libs + AOT-compiled assemblies for all four supported
ABIs). That is acceptable for the local qemu install path; the
Forgejo release workflow builds Release APKs separately and is
unaffected.
Validated end-to-end on this machine: AVD boots in 109s, the
build produces an APK with lib_*.dll.so for x86_64 (125 MB),
uninstall + reinstall + am start no longer aborts at
monodroid-glue.cc:757 (next test will confirm the app actually
renders, this commit only fixes the Fast Deployment crash).
Also adds qemu-logcat-boot target from the previous edit
(unchanged, documented in this commit message for context).
Targets for building and installing PostIt.Android (Debug) on the
local postit_test_avd AVD without leaving the terminal:
make qemu # run AVD -> wait boot -> build APK -> install
make qemu-install # (re)build APK + install (AVD must be running)
make qemu-build # build APK alone (no install)
make qemu-run # start the AVD in the background
make qemu-wait-boot # block until sys.boot_completed=1 (180s timeout)
make qemu-stop # adb emu kill
Defaults match the local setup: AVD postit_test_avd on x86_64
(android-x64 RID), adb on emulator-5554, Android SDK at
/opt/android-sdk. All overridable on the command line:
make qemu POSTIT_RID=android-arm64 ADB_SERIAL=emulator-5556
EMU_HEADLESS=1 disables the emulator window for scripted runs.
qemu-run logs to /tmp/yavsc-emu/<avd>.log.
Validated end-to-end on this machine: AVD booted in 109s on a
loaded system, APK built and installed cleanly. The 'UI not
responsive' warning is the software-rendering fallback when KVM
is busy; it does not block the install.
TestWebApplicationFactory instances shared the same in-memory database
because EF Core's UseInMemoryDatabase("InMemory") returns the same
backing store to every DbContext that asks for it under the same
connection string, in the same process. Whichever fixture started
first defined the state, and every subsequent fixture inherited it,
making tests silently order-dependent and flaky.
Fix:
- Yavsc.Tests.Shared/InMemoryDatabaseName: helper that suffixes the
in-memory connection string with a per-fixture GUID.
- TestWebApplicationFactory: instance GUID + ConnectionStrings__
YavscConnection set as an environment variable in the constructor
and cleared in Dispose, so each factory gets its own backing store.
Env var is needed because IdentityServer8.EntityFramework exposes
ConfigureDbContext as Action<DbContextOptionsBuilder> with no
service-provider access, so the connection string is captured at
registration time. AddEnvironmentVariables is the last provider in
the config pipeline and wins regardless.
- WebServerFixture: process-static GUID (WebHostFixture is a
per-process singleton by design, so the test collection shares one
store; the GUID still isolates from TestWebApplicationFactory).
- AddIdentityDBAndStores: read the connection string at DbContext
construction time via the (sp, options) overload of AddDbContext,
so test fixtures can override it via the host's IConfiguration.
IdentityServer stores cannot do the same without subclassing the
framework's DbContexts; the env var path is the documented escape
hatch in HostingExtensions.AddIdentityServer.
- UsesInMemoryProvider: StartsWith instead of equality, so
'InMemory-{guid}' is still recognised as an in-memory connection
string.
Regression sentinel in
Controllers/TestWebApplicationFactoryIsolationTests: two factories
seed a marker client in the first, the second must not see it.
Suite: 45/45 over 3 stable runs, 13-15s each.
xUnit1051 in two cases that call
EstimateSignatureFileHelper.ReceiveEstimateSignatureAsync through
Assert.ThrowsAsync lambdas. The lambda body runs on a different
stack frame, so capturing TestContext.Current.CancellationToken
in a local variable before the lambda is required — otherwise
xUnit1051 still flags the call (the implicit 'default' from
the parameter default lives in the lambda's scope, not the
test's).
The 2 xUnit1013 warnings on BaseTestContext.GitClone remain —
unrelated, about visibility vs [Fact] attribute on a helper
method, structural cleanup for another commit.
xUnit1051: HTTP helpers (GetAsync, PostAsJsonAsync, PutAsJsonAsync,
DeleteAsync) accept a CancellationToken that the test runner can use
to cancel a long-running suite. Forwarding TestContext.Current.
CancellationToken to every call lets the runner respond to Ctrl+C /
--blame-hang-timeout at the granularity of a single test instead of
the whole process.
Covers PublishEndpointTests (10 calls), CircleMembersApiTests
(11 calls) and BlogApiMappedClaimsTests (9 calls). BlogApiTests.cs
was already clean after 1868ed86.
The Forgejo release workflow validates the section title against the
channel derived from tag parity: '[TAG] - stable', '[TAG] - preview',
or '[TAG] - unstable'. The 2026-08-21 entry broke the validation with
'## [1.0.8-rc1] - 2026-08-21' (date suffix instead of channel). Move
the date into the body of the section (it is already mentioned in the
'Fixed' subsection) and use '## [1.0.8-rc1] - unstable' to match the
1.0.6 / 1.0.7 convention.
Ajoute la section [1.0.8-rc1] au CHANGELOG.md, en français, au format
Keep a Changelog (### Added / ### Changed / ### Fixed). Couvre :
- le fix backend du 500 sur POST /api/v1/blogacl (commit d2a0c263)
- le fix client PostIt (commit b82b6722)
- le passage de CheckOwner en async (commit e48ede1e)
- le seed one-shot de la fixture BlogsWebServerFixture
- les tests de non-régression (sentinelles 'never 500' et 'shape PostIt')
- la règle 'Pas de object' dans CONTRIBUTING.md
Met aussi à jour le bloc de liens de comparaison en bas du fichier
pour pointer [1.0.8-rc1] vers 1.0.7...1.0.8-rc1.
The 2026-08-21 prod 500 on POST /api/v1/blogacl was caused by the
PostIt client sending { circleId } only — the server deserialised
into CircleAuthorizationToBlogPost with BlogPostId = default(long) = 0,
and EF Core refused the INSERT with InvalidOperationException.
The PostIt-side fix lives in b82b6722 (enrich the payload with
blogPostId). This commit is the server-side guard: validate
BlogPostId > 0 in the controller and return 400 BadRequest instead
of letting the request reach SaveChangesAsync. The same shape that
crashed on 2026-08-21 now fails fast at the validation layer.
Verified by BlogAclApiTests.PostCircleAuthorization_dosent_return_500:
sentinel that asserts 'never 500' on a payload with BlogPostId = -1.
Previously red (500 from EF Core), now green (400 from the new guard).
The hard rule on POST /api/v1/blogacl is: a 500 is never acceptable,
regardless of the payload shape. The prod 500 logged on 2026-08-21 on
mercure was caused by the PostIt client sending { circleId } only, which
the server deserialised into CircleAuthorizationToBlogPost with
BlogPostId = default(long) = 0; EF Core refused the INSERT with
InvalidOperationException: The value of
'CircleAuthorizationToBlogPost.BlogPostId' is unknown. The PostIt fix
lives in b82b6722 (enrich the payload with blogPostId). The server-side
guard lives in this commit:
- BlogAclApiController.CheckOwner is now async and uses FirstOrDefaultAsync
instead of First, so it does not deadlock the request thread and
returns false on a missing circle (which the controller already maps
to ChallengeResult).
- BlogsWebServerFixture now seeds Alice, her Circle and her BlogPost
in ConfigurePipelineAsync, once at host startup, against the shared
SqliteConnection (Cache=Shared). EnsureCreated is idempotent and
runs against the connection that every DbContext resolves through,
so the test theory can POST payloads with real FK ids against a
schema that actually has the Circle / BlogSpot tables.
- BlogAclApiTests:
- PostCircleAuthorization_returns_201_when_payload_mirrors_PostIt_shape
is the regression sentinel for the prod fix.
- PostCircleAuthorization_never_returns_500 is a [Theory] over
several payload shapes; any future commit that reintroduces a
500 path turns it red. CleanupAcl at the start of each insert-
bearing test isolates against xUnit's no-guarantee-of-order
execution: a successful POST in test N would otherwise conflict
with test N+1 against the same (CircleId, BlogPostId) pair.
The bool Comment on CircleAuthorizationToBlogPost was dead code:
never read or written by any caller in src/, no UI exposure, no
behavioural semantics. The wire DTO (CircleAuthorization in
Yavsc.Abstract) doesn't carry it, no reader consumes it, and the
PostIt client builds its payload without it.
What changes:
- src/Yavsc.Server/Models/Access/CircleAuthorizationToBlogPost.cs:
remove the property.
- src/Yavsc.Blogs.Tests/BlogAclApiTests.cs: drop 'Comment = true'
from the existing test payload and trim the now-inaccurate XML
doc comment ('CircleId + BlogPostId + Comment' -> 'CircleId +
BlogPostId'). Also adds a new [Fact] pinning the prod bug
reported on 2026-08-21 (HTTP 500 'BlogPostId is unknown' when
PostIt POSTs the bare { circleId } shape). That test stays red:
the real fix for the 500 is in PostIt (payload needs blogPostId)
+ on the wire DTO + server-side validation, and lives in a
follow-up commit.
Migration:
- src/Yavsc.Org/Migrations/20260820232152_DropCommentFromCircleAuthorizationToBlogPost
drops the boolean 'Comment' column on CircleAuthorizationToBlogPost.
The generated scaffold also wanted to drop three 'ClientId1'
shadow FK columns on ClientScopes / ClientRedirectUris /
ClientGrantTypes (from leftover HasOne<Client>() overrides in
ApplicationDbContext.OnModelCreating); those were removed from
the .cs to keep the migration scoped to this fix. Cleaning up the
shadow property declarations themselves is left as a separate
task.
The ModelSnapshot still reflects the shadow 'ClientId1' columns
intentionally: they exist in the prod database today (all NULL),
and EF will rescaffold a drop migration for them on the next
'migrations add' regardless. No data loss.
Bundled end-of-branch commit on feat/postit-acl-members.
PostIt UI for circles + per-post ACL
- Reorganise PostIt.Tests into Auth/ and Blogs/ subfolders
(Bearer/OIDC scope tests vs. blog API fakes live where they
belong) and introduces PostItHeadlessCollection so the
Avalonia.Headless tests share a single xUnit collection
instead of contending with the EF-Core test host.
- Adds BlogAclApiTests (a brand-new behavioural layer over
POST /api/v1/blogacl) and the fakes it relies on
(BlogApiTestFakes, BlogPostAuthorDtoTests, AddCircleMember
DialogTests); pulls UserId-through-OIDC-sub path into
BearerScopeTests / FakeAuthorizingBrowser /
OidcStubAuthority.
- App.axaml.cs gets a small PushPageAsync touch-up the new
tests rely on.
- Drops UnitTest1.cs (xUnit scaffold, never used).
Yavsc.Blogs.Tests — SQLite instead of InMemory
- Bumps Yavsc.Blogs.Tests.csproj on
Microsoft.EntityFrameworkCore.Sqlite and rewrites
BlogsWebServerFixture to hold a single shared
SqliteConnection (Cache=Shared) for the fixture lifetime,
with a sync Dispose close to dodge async teardown hangs.
Reason: the EF Core InMemory provider silently ignores FKs,
which masked the kind of bug we are about to pin in the
ACL tests. SQLite enforces them, so any future INSERT that
forgets to seed its parent rows fails loudly here instead
of passing the test and breaking prod.
- PublishEndpointTests and BlogApiSmokeTests get a one-line
tweak to follow the new connection lifecycle.
Foreign-key fallout: seed the default user in the fixture
- Adds BlogsWebServerFixture.SeedUser(userName). Now that
SQLite enforces BlogPost.AuthorId → AspNetUsers.Id, every
test that POST/PUT/DELETE a BlogPost and sends AuthorId=
'tester' in the payload needs an AspNetUsers row to satisfy
the FK or it returns 500 with SQLite Error 19.
- BlogApiTests wraps the existing ResetDatabase with a
ResetAndSeedDefaultUser helper for the six mutating tests;
the four GET-only and ModelState-only tests keep the bare
ResetDatabase.
- Side benefit: every test in Yavsc.Blogs.Tests now finishes
cleanly instead of hanging at teardown — previously a stuck
test held the shared SqliteConnection open and the next
tests waited indefinitely.
Verified: dotnet test src/Yavsc.Blogs.Tests passes 25/25
green from a clean run, no fixture teardown hang.