DocumentStructure has useful graph-validation, tenant-store, and read-side tests. Commercial GA must additionally prove that Apply cannot leave half a tree, favorites cannot reveal newly forbidden resources, purge cannot bypass retention/legal hold, and both ORM adapters preserve the same behavior.
1. Current automated evidence
| Test surface | Covered today |
|---|---|
| command | duplicate node IDs, missing parent, returned persistent IDs |
| query | template-list store delegation, recycle-bin port delegation |
| store | EffectiveTenant propagation, favorite primary-key tenant predicate, old-node snapshot |
| architecture | ORM-neutral Infrastructure, default port, cross-module adapter placement |
| integration | DocumentStructure store parity participates in dual-ORM infrastructure |
Missing evidence includes real HTTP permissions, cycle/depth boundaries, concurrent name/update behavior, Apply target authorization, partial-failure rollback, Scriban security/cancellation, favorite target authorization, restore/purge races, capacity, and recovery.
2. Minimum regression set
# Application graph validation and read use cases.dotnet test tests/BitzOrcas.Application.Tests/BitzOrcas.Application.Tests.csproj \ --filter "FullyQualifiedName~DocumentStructure"
# Store and Documents recycle-bin unit evidence.dotnet test tests/BitzOrcas.Unit.Tests/BitzOrcas.Unit.Tests.csproj \ --filter "FullyQualifiedName~DocumentStructure|FullyQualifiedName~DocumentRecycleBinStore"
# Architecture boundary and default-port checks.dotnet test tests/BitzOrcas.Architecture.Tests/BitzOrcas.Architecture.Tests.csproj \ --filter "FullyQualifiedName~DocumentStructure"A release candidate must also run template multi-table transactions, unique conflicts, and recycle cascades against real SqlSugar and EF Core databases. Substitute stores are not release evidence.
3. HTTP authorization matrix
| Scenario | Expected result |
|---|---|
| anonymous request to any route | 401 or canonical authentication error |
| no template View permission | list/detail/preview denied |
| View but no Apply | apply denied |
| tenant A uses tenant B TemplateId | not found |
| template permission but no target-KB management | apply denied with zero categories |
| favorite target is forbidden | add denied |
| knowledge base is forbidden | recycle query denied |
| ordinary Delete tries purge | high-risk policy denial |
Tests must confirm request-type conventions resolve to the intended docs.folder-template.* and docs.recycle-bin.* resources, and must establish explicit favorite permissions. The presence of IAuthorizedRequest alone proves neither fact.
4. Graph and version tests
Cover blank ID, duplicate ID, self-cycle, two-node cycle, missing parent, depth ten success/eleven failure, 1/100/1000 nodes, sibling SortOrder ties, oversized fields, blank Name, and unknown TargetType. Update tests must add malformed version text, concurrent writers, unique-name races, snapshot schema, and rollback when delete or insert fails.
// Inject a persistence failure after deleting old nodes but before inserting new nodes.storeFaults.FailNextNodeInsert(new IOException("storage unavailable"));
var result = await sender.Send(updateCommand, cancellationToken);
result.IsFailure.ShouldBeTrue();// Root, CurrentVersion, current nodes, and history must remain mutually consistent.await evidence.AssertTemplateVersionAsync( updateCommand.Id, "1.0.0", expectedOldNodes, cancellationToken);5. Apply fault injection
Inject failures at target authorization, Compile, Render, first/middle/final category save, transaction commit, and immediately before response. Every case must assert zero partial tree or completed compensation, recoverable idempotency state, no duplicates after replay, and a stable error code that does not expose NamingContext.
// Every retry for one business intent reuses the same application ID.var command = NewApplyCommand(applicationId: "apply-2026-0042");
// Start both requests together to exercise the database uniqueness guard.var results = await Task.WhenAll( sender.Send(command, cancellationToken).AsTask(), sender.Send(command, cancellationToken).AsTask());
results.ShouldAllBe(result => result.IsSuccess);results[0].Value!.CreatedCategoryIds.ShouldBe(results[1].Value!.CreatedCategoryIds);await evidence.AssertSingleTreeAsync( command.TargetId, command.ApplicationId, cancellationToken);ApplicationId does not exist in the current command. This is an explicit GA target test, not a current usage example.
6. Scriban security tests
Test unknown variables, syntax errors, huge output, deep loops, timeout, cancellation, sensitive context, forbidden functions/objects, Unicode, and colliding rendered names. Preview returns per-node diagnostics; Apply performs zero writes if any required expression fails.
Capture compile/render duration and stable error code, never secrets or full context. If compiled templates are cached, the key must include a safe tenant boundary, expression hash, engine, and policy version.
7. Favorite tests
Cover RequireUserId, allowed target types, missing target, cross-tenant target, current denial, later permission loss/deletion, duplicate active add, re-add after soft delete, concurrent duplicates, group filter, own/other-user delete, and paged large lists. Both ORMs must agree on unique and soft-delete restoration behavior.
// Create the relation first, then change owner authorization independently.await favorites.AddAsync(userId, "Document", documentId, cancellationToken);await permissions.RevokeDocumentReadAsync(userId, documentId, cancellationToken);
var page = await sender.Send(new ListFavorites.Query(Page: 1, PageSize: 20), cancellationToken);
page.Items.ShouldNotContain(item => item.TargetId == documentId && item.ResolvedTitle is not null);The paged and resolved query in this example is a target contract; the current query returns raw relations without pagination.
8. Recycle-bin tests
For both Document and Category, cover missing, not deleted, cross-knowledge-base, cross-tenant, deleted parent, name collision, category subtree, attachments/versions, search index, favorite references, repeated restore, repeated purge, restore/purge race, retention, and legal hold.
An interrupted purge needs a resumable job/report. Event consumers must replay safely without deleting external objects twice.
9. Capacity baselines
At minimum measure templates with 1/10/100/1000 nodes, tenants with 10/1,000/100,000 templates, users with 10/1,000/10,000 favorites, recycle bins with 10,000/1,000,000 items, and category cascades of 10/1,000/100,000 objects. Capture P50/P95/P99, database round trips, transaction log, locks, allocations, compile duration, and event backlog.
Template list currently calls CountTemplateNodesAsync once per template, creating an N+1 pattern. Replace it with a grouped read-store query before large-tenant GA.
10. Metrics and alerts
- template create/update/delete/conflict;
- graph validation error by code, node count, and depth;
- preview compile/render/fallback duration;
- apply duration and planned/created/failed/compensated nodes;
- application-id replay/conflict;
- favorite add/conflict/unresolved/denied/list size;
- recycle query/restore/purge, cascade size, and legal-hold denial;
- transaction rollback, outbox lag, and dead letter.
Alert especially on partial Apply, failed compensation, unauthorized/failed purge, unresolved-favorite spikes, Scriban timeout, and abnormal cross-tenant denials.
11. Backup and recovery
Backups include template roots, current nodes, history snapshots, favorites, Documents recycle-bin facts, and related Outbox records. After restore, verify parent references, CurrentVersion/snapshot consistency, favorite natural-key uniqueness, soft-delete filtering, recycle-tree integrity, and that Search/Files consumers can rebuild.
Historical snapshots have no schema version today. GA needs an old-snapshot migration and an isolation path for snapshots that cannot be parsed.
12. Release blockers
- Apply does not validate the target instance or TargetType.
- Per-node writes lack transaction/compensation fault evidence.
- Unresolved queued nodes can produce partial success.
- No application idempotency key exists.
- Scriban blocks async, ignores cancellation, and swallows errors.
- Versions have no query/diff/rollback/schema version/concurrency control.
- Favorites do not validate targets or permission; UserId may become
"0". - Favorite permissions are not explicitly governed.
- Recycle object authorization and purge retention/legal hold are insufficient.
- Cross-module restore/purge event convergence is unproven.
- Template-list N+1 and large-tree capacity have no baseline.
- HTTP, dual-ORM, concurrency, fault, recovery, and operations evidence is incomplete.
13. Delivery sequence
P0 covers target authorization, atomic or compensatable Apply, complete results, idempotency, asynchronous Scriban, favorite owner authorization, and recycle object policy. P1 adds the version product, concurrency control, batched reads, purge jobs and retention, integration events, and observability. Shared templates, collaborative favorites, and richer blueprints are later product features.
14. Final sweeps
# High-risk current patterns; P0 should remove or register every remaining compatibility use.rg -n "GetAwaiter\(\)\.GetResult|CancellationToken.None|UserId\?\.ToString\(\) \?\? \"0\"" \ src/Platform/DocumentStructure -g '*.cs'
# Direct per-node cross-module write should disappear behind an owner contract.rg -n "IRepository<DocumentCategory>|SaveAsync\(category" \ src/Platform/DocumentStructure -g '*.cs'DocumentStructure overview · Template preview and apply · Favorites and recycle bin