feat(cordis): 完成阶段5访问介导与诊断

This commit is contained in:
2026-08-17 01:09:32 +08:00
parent de5ab449cd
commit 8738e633ee
35 changed files with 1540 additions and 50 deletions
@@ -95,6 +95,40 @@ namespace ShrinkContext.Tests
Assert.AreEqual(1, rootValueFinal);
}
[Test]
public void TypedKey_PreservesVersionedIdentityAcrossMediatedAccess()
{
var key = new ShrinkKey<string>("test.package", "typed-service", 2);
Assert.AreEqual("test.package/typed-service@v2", key.Id);
_runtime.Use(new PolicyProviderComponent(key.Id), "typed-value");
var consumer = _runtime.Use(new PolicyConsumerComponent(key.Id));
Assert.AreEqual(ShrinkFiberState.Active, consumer.State);
Assert.AreEqual("typed-value", consumer.Ctx.Get(key));
}
[Test]
public void Intercept_MergesRootToLeafAndChildMetadataWins()
{
const string key = "policy.service";
_runtime.Use(new PolicyProviderComponent(key), "value");
var consumer = _runtime.Use(new PolicyConsumerComponent(key));
Assert.AreEqual(ShrinkFiberState.Active, consumer.State);
var parent = consumer.Ctx.Intercept(key, new Dictionary<string, object?>
{
["prefix"] = "parent-",
["suffix"] = "-parent"
});
var child = parent.Intercept(key, new Dictionary<string, object?>
{
["suffix"] = "-child"
});
Assert.AreEqual("parent-value-child", child.Get<string>(key));
}
/// <summary>声明一个键却访问另一个未声明键的组件(验证能力介导纪律)。</summary>
private sealed class GreedyConsumerComponent : IShrinkComponent
{
@@ -1,6 +1,7 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using Cysharp.Threading.Tasks;
using NUnit.Framework;
using ShrinkContext;
@@ -155,6 +156,57 @@ namespace ShrinkContext.Tests
Assert.AreEqual(ShrinkFiberState.Active, firstFiber.State);
}
[Test]
public void UndeclaredSupply_FailsFiberAndLeavesNoBinding()
{
var fiber = _runtime.Use(new UndeclaredProviderComponent());
Assert.AreEqual(ShrinkFiberState.Inactive, fiber.State);
Assert.IsInstanceOf<ShrinkUndeclaredSupplyException>(fiber.LastError);
Assert.IsFalse(_runtime.TryGetRaw<int>(_runtime.RootContext, "not-declared", out _));
var snapshot = _runtime.CaptureDiagnostic();
var diagnostic = snapshot.Fibers.Single(item => item.Uid == fiber.Uid);
Assert.AreEqual(ShrinkFiberDiagnosticStatus.Failed, diagnostic.Status);
StringAssert.Contains("not-declared", diagnostic.ErrorMessage);
}
[Test]
public void DiagnosticSnapshot_ExplainsWaitingDependencyAndPotentialProvider()
{
var waiting = _runtime.Use(new ConsumerComponent("waiting", "svc"));
var blockedProvider = _runtime.Use(new ProviderComponent("blocked-provider", "svc")
{
Inject = new[] { "missing" }
});
var snapshot = _runtime.CaptureDiagnostic();
var waitingDiagnostic = snapshot.Fibers.Single(item => item.Uid == waiting.Uid);
Assert.AreEqual(ShrinkFiberDiagnosticStatus.Waiting, waitingDiagnostic.Status);
Assert.AreEqual(1, waitingDiagnostic.Dependencies.Count);
Assert.IsFalse(waitingDiagnostic.Dependencies[0].IsSatisfied);
CollectionAssert.Contains(waitingDiagnostic.Dependencies[0].PotentialProviderUids,
blockedProvider.Uid,
"等待链应显示声明了该键、但自身依赖未满足的潜在提供者");
}
[Test]
public void NotifyIndex_VisitsOnlyConsumersOfChangedKey()
{
var target = new ConsumerComponent("target", "target-key");
_runtime.Use(target);
for (var i = 0; i < 40; i++)
_runtime.Use(new ConsumerComponent("unrelated-" + i, "other-" + i));
_runtime.Use(new ProviderComponent("provider", "target-key", "value"));
var notifications = _runtime.CaptureDiagnostic().Notifications;
Assert.AreEqual(1, notifications.LastCandidateCount,
"notify 应从 key 倒排索引取得候选,不扫描 40 个无关 fiber");
Assert.AreEqual(41, notifications.IndexedKeyCount);
Assert.AreEqual("value", target.LastSeen);
}
[Test]
public void CircularDependencies_BothStayInactiveWithoutError()
{
@@ -24,6 +24,9 @@ namespace ShrinkContext.Tests
_catalog.Register("provider", () => new ConfigProviderComponent("provider", "svc"));
_catalog.Register("provider.alt", () => new ConfigProviderComponent("provider.alt", "svc"));
_catalog.Register("consumer", () => new ConsumerComponent("consumer", "svc"));
_catalog.Register("failing", () => new FailingComponent("failing", "svc"));
_catalog.Register("policy.provider", () => new PolicyProviderComponent("policy.service"));
_catalog.Register("policy.consumer", () => new PolicyConsumerComponent("policy.service"));
_loader = new ShrinkContextLoader(_runtime, _catalog);
}
@@ -223,5 +226,87 @@ namespace ShrinkContext.Tests
Assert.AreEqual("from-r2", ((ConsumerComponent)consumerFiber.Component).LastSeen,
"消费者按自身隔离域解析到 r2 的绑定");
}
[Test]
public void IsolatedProviderReplacement_DoesNotReloadOtherRealmConsumer()
{
var isolateR1 = new Dictionary<string, string> { ["svc"] = "r1" };
var isolateR2 = new Dictionary<string, string> { ["svc"] = "r2" };
TestAwait.Run(_loader.ApplyAsync(new List<ShrinkLoaderEntry>
{
new("p1", "provider", "r1-v1", isolate: isolateR1),
new("p2", "provider", "r2-v1", isolate: isolateR2),
new("c2", "consumer", isolate: isolateR2),
}));
Assert.IsTrue(_loader.TryGetFiber("c2", out var consumerFiber));
var consumer = (ConsumerComponent)consumerFiber.Component;
Assert.AreEqual(1, consumer.LoadCount);
TestAwait.Run(_loader.ApplyAsync(new List<ShrinkLoaderEntry>
{
new("p1", "provider", "r1-v2", isolate: isolateR1),
new("p2", "provider", "r2-v1", isolate: isolateR2),
new("c2", "consumer", isolate: isolateR2),
}));
Assert.AreEqual(1, consumer.LoadCount,
"r1 提供者替换不得触发只解析 r2 的消费者重载");
Assert.AreEqual("r2-v1", consumer.LastSeen);
}
[Test]
public void InterceptUpdate_ChangesAccessWithoutReloadingFiber()
{
var suffixA = new Dictionary<string, IReadOnlyDictionary<string, object?>>
{
["policy.service"] = new Dictionary<string, object?> { ["suffix"] = "-a" }
};
var suffixB = new Dictionary<string, IReadOnlyDictionary<string, object?>>
{
["policy.service"] = new Dictionary<string, object?> { ["suffix"] = "-b" }
};
TestAwait.Run(_loader.ApplyAsync(new List<ShrinkLoaderEntry>
{
new("policy-provider", "policy.provider", "base"),
new("policy-consumer", "policy.consumer", intercept: suffixA),
}));
Assert.IsTrue(_loader.TryGetFiber("policy-consumer", out var firstFiber));
var consumer = (PolicyConsumerComponent)firstFiber.Component;
Assert.AreEqual("base-a", consumer.Read());
Assert.AreEqual(1, consumer.ApplyCount);
TestAwait.Run(_loader.ApplyAsync(new List<ShrinkLoaderEntry>
{
new("policy-provider", "policy.provider", "base"),
new("policy-consumer", "policy.consumer", intercept: suffixB),
}));
Assert.IsTrue(_loader.TryGetFiber("policy-consumer", out var sameFiber));
Assert.AreSame(firstFiber, sameFiber);
Assert.AreEqual(1, consumer.ApplyCount, "intercept 元数据变化不应触发 reload");
Assert.AreEqual("base-b", consumer.Read(), "访问时应读取更新后的 metadata");
}
[Test]
public void FailedReplacement_RestoresPreviousLoaderCompositionAndReportsTransaction()
{
TestAwait.Run(_loader.ApplyAsync(new List<ShrinkLoaderEntry>
{
new("provider-entry", "provider", "stable")
}));
Assert.Throws<ShrinkLoaderException>(() => TestAwait.Run(_loader.ApplyAsync(
new List<ShrinkLoaderEntry> { new("provider-entry", "failing") })));
Assert.IsTrue(_loader.TryGetFiber("provider-entry", out var restored));
Assert.AreEqual(ShrinkFiberState.Active, restored.State);
Assert.AreEqual("stable", restored.Config);
Assert.IsNotNull(_loader.LastTransaction);
Assert.AreEqual(ShrinkLoaderTransactionPhase.Failed, _loader.LastTransaction!.Phase);
Assert.IsTrue(_loader.LastTransaction.RestoreAttempted);
Assert.IsTrue(_loader.LastTransaction.PreviousCompositionRestored);
StringAssert.Contains("failed during apply", _loader.LastTransaction.ErrorMessage);
}
}
}
@@ -250,6 +250,81 @@ namespace ShrinkContext.Tests
}
}
/// <summary>故意写入未声明键,验证 Provide 运行时契约。</summary>
public sealed class UndeclaredProviderComponent : IShrinkComponent
{
public string Name => "undeclared-provider";
public IReadOnlyList<string> Inject => Array.Empty<string>();
public IReadOnlyList<string> Provide => Array.Empty<string>();
public UniTask ApplyAsync(ShrinkCtx ctx, object? config)
{
ctx.Set("not-declared", 1);
return UniTask.CompletedTask;
}
}
public sealed class MetadataSuffixPolicy : IShrinkCoeffectAccessPolicy
{
public object? Resolve(ShrinkCoeffectAccessContext context, object? value)
{
var text = value?.ToString() ?? string.Empty;
if (context.TryGetMetadata<string>("prefix", out var prefix))
text = prefix + text;
return context.TryGetMetadata<string>("suffix", out var suffix) ? text + suffix : text;
}
}
public sealed class PolicyProviderComponent : IShrinkComponent
{
private readonly string[] _provide;
public PolicyProviderComponent(string key)
{
Key = key;
_provide = new[] { key };
}
public string Key { get; }
public string Name => "policy-provider";
public IReadOnlyList<string> Inject => Array.Empty<string>();
public IReadOnlyList<string> Provide => _provide;
public UniTask ApplyAsync(ShrinkCtx ctx, object? config)
{
ctx.Set(Key, config?.ToString() ?? "value", new MetadataSuffixPolicy());
return UniTask.CompletedTask;
}
}
public sealed class PolicyConsumerComponent : IShrinkComponent
{
private readonly string[] _inject;
private ShrinkCtx? _ctx;
public PolicyConsumerComponent(string key)
{
Key = key;
_inject = new[] { key };
}
public string Key { get; }
public string Name => "policy-consumer";
public int ApplyCount { get; private set; }
public IReadOnlyList<string> Inject => _inject;
public IReadOnlyList<string> Provide => Array.Empty<string>();
public UniTask ApplyAsync(ShrinkCtx ctx, object? config)
{
_ctx = ctx;
ApplyCount++;
return UniTask.CompletedTask;
}
public string Read() => (_ctx ?? throw new InvalidOperationException("Consumer is not active."))
.Get<string>(Key);
}
/// <summary>测试辅助:同步等待 UniTask(测试保证全部延续同步完成,无死锁风险)。</summary>
public static class TestAwait
{