Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions fkst.lock
Original file line number Diff line number Diff line change
@@ -1,35 +1,35 @@
[[external_source]]
id = "fkst-packages"
git = "https://github.com/eanz17/fkst-packages.git"
git = "https://github.com/ChronoAIProject/fkst-packages.git"

[external_source.intent]
rev = "16c14ae0d84fd0b0bbc0e93cdd75320a36c3ba42"
rev = "7e1defb04493d6f4478c569f4fe2b6aae2118b4b"

[external_source.resolved]
rev = "16c14ae0d84fd0b0bbc0e93cdd75320a36c3ba42"
tree_sha256 = "sha256-4daa7da2dcd818ed948a36eca1c8272aa9bbcfc781698e49d64fc4ced0d8e239"
rev = "7e1defb04493d6f4478c569f4fe2b6aae2118b4b"
tree_sha256 = "sha256-6e957d4d303a511e4aa53270198158a0da8916bfa3ca01d456f3f51f94989703"

[[external_source.libraries]]
name = "contract"
unit = "libraries/contract"
exports_sha256 = "sha256-c600e57cc7529db6cb2d75119b70c335ae5ffb752aa2b594d16aa09a508c9a19"
exports_sha256 = "sha256-7c47fe42b1fb8fc75e75f18fdbb35397826db6650719db1548aed222bdb83308"

[[external_source.libraries]]
name = "forge"
unit = "libraries/forge"
exports_sha256 = "sha256-2b0b799b77be3318d4405cc6a988b90c9929bc31e15da3ede20cbe45043efc63"
exports_sha256 = "sha256-2dc39c440779cfc6ffa70113eff50c4c11cce0fbf502d7e42229e62d53bd04b1"

[[external_source.libraries]]
name = "testkit"
unit = "libraries/testkit"
exports_sha256 = "sha256-e8b9b223638f453299ae24b898f2aad51d0ce76ea7edc29e0ffc2bb1a1d85204"
exports_sha256 = "sha256-18574f619e2c2cfefdc501d4ee03ca1b0eb6864a44980a92c79ebc0cc7bc789a"

[[external_source.libraries]]
name = "workflow"
unit = "libraries/workflow"
exports_sha256 = "sha256-4b33c2f9e7002470fa6f9db800fa5f8e137e19a5f3a717ed2b2d027c90af5065"
exports_sha256 = "sha256-2415269fbf6ee636a3377de3e8fc6a13573f4511553010c871db7ece3fb646b1"

[[external_source.libraries]]
name = "devloop"
unit = "libraries/devloop"
exports_sha256 = "sha256-288aa3dbf1b1324c3ad4e6b86a4c2bef448b12ae2f66ddd53f99e8384d023ebb"
exports_sha256 = "sha256-0c9df412d166f76e74cc40741c303724c2b977bae8e3b58461ba195e3a03114e"
4 changes: 2 additions & 2 deletions fkst.workspace.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ workspace = "workspace"

[[external_sources]]
id = "fkst-packages"
git = "https://github.com/eanz17/fkst-packages.git"
rev = "16c14ae0d84fd0b0bbc0e93cdd75320a36c3ba42"
git = "https://github.com/ChronoAIProject/fkst-packages.git"
rev = "7e1defb04493d6f4478c569f4fe2b6aae2118b4b"
libraries = ["contract", "forge", "testkit", "workflow", "devloop"]
packages = [
"github-proxy",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# State Output Obligation Timeout Fixture

This fixture models the persisted blocked terminal from `#3361`'s parent issue.

<!-- fkst:github-devloop:state:v1 proposal="github-devloop/issue/aevatarAI/aevatar/3344" state="blocked" version="ready/consensus-github-devloop/issue/aevatarAI/aevatar/3344/intake/0366205136/loop/1/timeout-reconcile/impl-failed/3" stage_rank="800" marker_order_key="ready-consensus-github-devloo-001839198015/000000000001/000000000000/000000000000/000000000000/000000000000/000000000000/000000000000/000000000800" -->
<!-- fkst:github-devloop:timeout-reconcile:v1 proposal="github-devloop/issue/aevatarAI/aevatar/3344" version="ready/consensus-github-devloop/issue/aevatarAI/aevatar/3344/intake/0366205136/loop/1/timeout-reconcile/impl-failed/3" state="impl-failed" round="3" action="drop" dedup="timeout-reconcile:ready/consensus-github-devloop/issue/aevatarAI/aevatar/3344/intake/0366205136/loop/1/timeout-reconcile/impl-failed/3" from_state="impl-failed" from_version="ready/consensus-github-devloop/issue/aevatarAI/aevatar/3344/intake/0366205136/loop/1" age_minutes="1472" budget_minutes="1440" attempt="3" attempt_limit="3" driving_queue="devloop_ready" reason_class="state-output-obligation-timeout" source_ref_kind="external" source_ref="aevatarAI/aevatar#issue/3344" -->
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
using System.Diagnostics;

namespace Aevatar.Architecture.Tests.Rules;

public class FkstOutputObligationLivenessGuardTests
{
[Fact]
public async Task StateOutputObligationTimeoutFixtureDrainsIdempotently()
{
var repositoryRoot = FindRepositoryRoot();
var scriptPath = Path.Combine(repositoryRoot, "tools", "ci", "fkst_output_obligation_liveness_guard.sh");
var fixturePath = Path.Combine(
repositoryRoot,
"test",
"Aevatar.Architecture.Tests",
"Fixtures",
"Fkst",
"state-output-obligation-timeout-blocked.md");

var result = await RunBashAsync(scriptPath, fixturePath);

Assert.Equal(0, result.ExitCode);
Assert.Contains(
"state-output-obligation-timeout obligation owner path verified.",
result.Output);
Assert.Contains("verified_owner_package=github-devloop", result.Output);
Assert.Contains("verified_effect_package=github-proxy", result.Output);
Assert.Contains("verified_owner_test=restart_timeout_obligations_test", result.Output);
Assert.Contains("verified_reconciler_test=timeout_reconcile_cas_parity_test", result.Output);
Assert.Contains("verified_from_state=impl-failed", result.Output);
Assert.Contains("verified_source_test=liveness_timeout_attempt_issue_test", result.Output);
Assert.Contains(
"verified_source_case=test_impl_failed_retry_limit_replay_decline_climbs_to_timeout_reconcile_without_seeded_timeout_markers",
result.Output);
Assert.Contains(
"verified_incident_source_case=test_incident_impl_failed_timeout_source_is_redriven_from_fixture",
result.Output);
Assert.Contains(
"verified_incident_reconciler_case=test_incident_impl_failed_timeout_reconcile_skips_stale_terminal_drop",
result.Output);
Assert.Contains(
"verified_incident_terminal_case=test_incident_blocked_output_obligation_drains_once_from_fixture",
result.Output);
Assert.Contains("verified_effect_test=integration_issue_create_test", result.Output);
}

private static async Task<CommandResult> RunBashAsync(string scriptPath, string fixturePath)
{
var startInfo = new ProcessStartInfo
{
FileName = "bash",
RedirectStandardError = true,
RedirectStandardOutput = true,
UseShellExecute = false,
};
startInfo.ArgumentList.Add(scriptPath);
startInfo.ArgumentList.Add(fixturePath);

using var process = Process.Start(startInfo)
?? throw new InvalidOperationException("Failed to start bash process.");

var stdout = process.StandardOutput.ReadToEndAsync();
var stderr = process.StandardError.ReadToEndAsync();
await process.WaitForExitAsync();

return new CommandResult(
process.ExitCode,
string.Concat(await stdout, await stderr));
}

private static string FindRepositoryRoot()
{
var directory = new DirectoryInfo(AppContext.BaseDirectory);
while (directory is not null)
{
if (File.Exists(Path.Combine(directory.FullName, "aevatar.slnx")))
{
return directory.FullName;
}

directory = directory.Parent;
}

throw new InvalidOperationException("Could not locate repository root.");
}

private sealed record CommandResult(int ExitCode, string Output);
}
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,7 @@ public async Task InMemoryLedger_AfterRetentionCleanup_ShouldStartCurrentFactWit
currentAttempt.Status.Should().Be(AgentToolAdmissionStatus.Started);
}

[Fact]
[PinnedRedisFact]
public async Task GarnetStore_WithPinnedRedis_ShouldRoundTripBinaryAndExpireKey()
{
await using var server = await PinnedRedisServer.StartAsync();
Expand All @@ -322,7 +322,7 @@ public async Task GarnetStore_WithPinnedRedis_ShouldRoundTripBinaryAndExpireKey(
retention.Should().BePositive().And.BeLessThanOrEqualTo(TimeSpan.FromHours(24));
}

[Fact]
[PinnedRedisFact]
public async Task DistributedLedger_WithPinnedRedis_ShouldAtomicallyStartOnceThenRejectDuplicatesAndConflict()
{
await using var server = await PinnedRedisServer.StartAsync();
Expand All @@ -343,7 +343,7 @@ public async Task DistributedLedger_WithPinnedRedis_ShouldAtomicallyStartOnceThe
conflict.Status.Should().Be(AgentToolAdmissionStatus.Conflict);
}

[Fact]
[PinnedRedisFact]
public async Task GarnetStore_WhenCallerCancels_ShouldPropagateWithoutWriting()
{
await using var server = await PinnedRedisServer.StartAsync();
Expand Down Expand Up @@ -555,3 +555,32 @@ private static int ReservePort()
}
}
}

internal sealed class PinnedRedisFactAttribute : FactAttribute
{
private const string ConnectionStringEnvironmentVariable =
"AGENT_TOOL_ADMISSION_REDIS_CONNECTION_STRING";

public PinnedRedisFactAttribute()
{
if (string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable(ConnectionStringEnvironmentVariable)) &&
!RedisServerIsOnPath())
{
Skip =
"Set AGENT_TOOL_ADMISSION_REDIS_CONNECTION_STRING or put redis-server 7.2.3 on PATH to run pinned Redis admission ledger tests.";
}
}

private static bool RedisServerIsOnPath()
{
var path = Environment.GetEnvironmentVariable("PATH");
if (string.IsNullOrWhiteSpace(path))
{
return false;
}

return path.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries)
.Select(directory => Path.Combine(directory, "redis-server"))
.Any(File.Exists);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,10 @@ public static async Task<MainnetTestHost> StartAsync()
options.EnableConnectorBootstrap = false;
options.EnableCors = false;
});
builder.Services
.AddHttpClient("NyxIdAssistantActionRegistry")
.ConfigurePrimaryHttpMessageHandler(static () =>
new NyxIdAssistantActionRegistryHandler());
builder.Services.PostConfigure<JwtBearerOptions>(
JwtBearerDefaults.AuthenticationScheme,
options => ConfigureTestTokenValidation(options, signingKey));
Expand Down Expand Up @@ -497,6 +501,88 @@ public Task<NyxIdLlmService> ProvisionAsync(
throw new NotSupportedException("Provisioning is not part of the owner-scope security test.");
}

private sealed class NyxIdAssistantActionRegistryHandler : HttpMessageHandler
{
private const string RegistryJson = """
{
"schema_version": 4,
"revision": "nyxid-assistant-actions.v4",
"actions": [
{
"action": "service.connect",
"description": "Connect a service through the NyxID browser journey.",
"params_schema": {
"oneOf": [
{
"type": "object",
"additionalProperties": false,
"required": ["catalogService"],
"properties": {
"catalogService": {
"type": "object",
"additionalProperties": false,
"required": ["serviceSlug"],
"properties": {
"serviceSlug": {"type": "string"},
"requestedScopes": {
"type": "array",
"items": {"type": "string"}
},
"viaNodeId": {"type": "string"},
"targetOrgId": {"type": "string"}
}
}
}
},
{
"type": "object",
"additionalProperties": false,
"required": ["customService"],
"properties": {
"customService": {
"type": "object",
"additionalProperties": false,
"required": ["name", "endpointUrl", "authMethod"],
"properties": {
"name": {"type": "string"},
"endpointUrl": {"type": "string"},
"authMethod": {"type": "string"},
"authKeyName": {"type": "string"},
"viaNodeId": {"type": "string"},
"targetOrgId": {"type": "string"}
}
}
}
}
]
},
"risk": "grant",
"tier": "v1",
"remember_eligible": true
}
]
}
""";

protected override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
if (request.Method != HttpMethod.Get ||
request.RequestUri?.AbsolutePath != "/api/v1/assistant/actions" ||
request.Headers.Authorization is not null)
{
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.BadRequest));
}

return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(RegistryJson, Encoding.UTF8, "application/json"),
});
}
}

private sealed class TemporaryAevatarHomeScope : IDisposable
{
private readonly string? _previous;
Expand Down
Loading
Loading