M2: the four V1 atoms + Git sync (Gitea / filesystem)

- Author the four V1 skill atoms in skills/ (Git is the source of truth): spec-writing &
  story-breakdown (product-owner), test-plan-generation & diff-review (qa) — each with
  risk-tagged actions, golden tests, and a body.
- SharedKernel: IGitProvider seam (read-only, provider-agnostic) + GitFile.
- Integrations module (its first real code): FileSystemGitProvider (dogfood/local) and a
  GiteaGitProvider (Gitea REST: recursive tree → SKILL.md blobs → base64 contents); the
  provider is chosen by GitSource:Provider config.
- Skills: SkillSyncService consumes IGitProvider (never Integrations) and indexes each file;
  POST /api/skills/sync and a POST /api/skills/webhook/gitea (re-sync on push; signature
  verification + changed-file-only + queue offload come later).

Verified: build green; ArchitectureTests 8/8 (Skills & Integrations reference only
SharedKernel; the Git seam lives in SharedKernel); IntegrationTests 22/22 incl. a sync that
indexes the four real atoms from skills/, published and queryable by role.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-09 18:34:53 +03:30
parent 401e3e69af
commit bfcd223374
15 changed files with 452 additions and 5 deletions
@@ -0,0 +1,77 @@
using System.Net;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using Xunit;
namespace TeamUp.IntegrationTests;
/// <summary>
/// M2 acceptance: syncing from the Git source (the repo's skills/ dir via the filesystem provider)
/// indexes the four V1 atoms, published and queryable by their role.
/// </summary>
public sealed class SkillSyncTests(PostgresFixture postgres) : IClassFixture<PostgresFixture>
{
private sealed record BootstrapResponse(string Token, Guid MemberId, Guid OrganizationId);
private sealed record SyncResult(int Indexed);
private sealed record ActionDto(string Name, string Risk);
private sealed record SkillSummary(
string SkillKey, string Name, string Version, string? Summary, List<string> Roles,
string Visibility, string MinTier, string Status, List<ActionDto> Actions);
[Fact]
public async Task Sync_indexes_the_four_atoms_queryable_by_role()
{
var settings = new Dictionary<string, string?>
{
["GitSource:Provider"] = "filesystem",
["GitSource:Root"] = LocateSkillsDirectory(),
};
await using var factory = new TeamUpWebFactory(postgres.ConnectionString, settings);
using var anon = factory.CreateClient();
var bootstrap = await anon.PostAsJsonAsync("/api/identity/bootstrap", new
{
organizationName = "AliaSaaS",
ownerEmail = "owner@alia.test",
ownerDisplayName = "Owner",
ownerPassword = "Passw0rd!",
});
var owner = await bootstrap.Content.ReadFromJsonAsync<BootstrapResponse>();
Assert.NotNull(owner);
using var client = factory.CreateClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", owner!.Token);
var syncResponse = await client.PostAsync("/api/skills/sync", content: null);
Assert.Equal(HttpStatusCode.OK, syncResponse.StatusCode);
var result = await syncResponse.Content.ReadFromJsonAsync<SyncResult>();
Assert.Equal(4, result!.Indexed);
var productOwner = await client.GetFromJsonAsync<List<SkillSummary>>("/api/skills/?role=product-owner");
Assert.Contains(productOwner!, s => s.SkillKey == "spec-writing");
Assert.Contains(productOwner!, s => s.SkillKey == "story-breakdown");
Assert.All(productOwner!, s => Assert.Equal("Published", s.Status));
var qa = await client.GetFromJsonAsync<List<SkillSummary>>("/api/skills/?role=qa");
Assert.Contains(qa!, s => s.SkillKey == "test-plan-generation");
Assert.Contains(qa!, s => s.SkillKey == "diff-review");
}
private static string LocateSkillsDirectory()
{
var dir = new DirectoryInfo(AppContext.BaseDirectory);
while (dir is not null && !File.Exists(Path.Combine(dir.FullName, "TeamUp.slnx")))
{
dir = dir.Parent;
}
Assert.NotNull(dir);
var skills = Path.Combine(dir!.FullName, "skills");
Assert.True(Directory.Exists(skills), $"skills directory not found at {skills}");
return skills;
}
}
@@ -7,7 +7,9 @@ namespace TeamUp.IntegrationTests;
/// Drives the real <see cref="Program"/> web host against the test container, in Development so
/// migrations apply on startup and the OpenAPI document is mapped.
/// </summary>
public sealed class TeamUpWebFactory(string connectionString) : WebApplicationFactory<Program>
public sealed class TeamUpWebFactory(
string connectionString,
IReadOnlyDictionary<string, string?>? settings = null) : WebApplicationFactory<Program>
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
@@ -15,5 +17,13 @@ public sealed class TeamUpWebFactory(string connectionString) : WebApplicationFa
builder.UseSetting("ConnectionStrings:Postgres", connectionString);
builder.UseSetting("Database:ApplyMigrationsOnStartup", "true");
builder.UseSetting("OpenTelemetry:OtlpEndpoint", string.Empty);
if (settings is not null)
{
foreach (var (key, value) in settings)
{
builder.UseSetting(key, value);
}
}
}
}