Skip to content

Commit fcd5a29

Browse files
ScottArbeitScott Arbeit
andauthored
Fix grace connect end-to-end (bootstrap config + real download + version selection) (#49)
* Add continuous review foundation layers * Add work item CLI commands * Add review CLI commands * Add queue CLI commands * Add determinism and queue tests * Add continuous review tests and docs * Update beads tracking * bd sync: 2026-01-06 21:10:01 * Close Grace-7it epic * Update beads metadata * Add auth failure logging * Log principals on auth failures * Close Grace-vz3 * Fixes for `grace connect`; logging improvements; authentication improvements * Update beads tracking * bd sync: 2026-01-06 21:10:01 * Close Grace-7it epic * Add auth failure logging * Log principals on auth failures * Fixes for `grace connect`; logging improvements; authentication improvements * Fix Spectre console output capture in CLI tests * Allow connect without config when parse errors occur * Fix duplicate captureOutput in help tests --------- Co-authored-by: Scott Arbeit <scottarbeit@github.com>
1 parent b54e4e4 commit fcd5a29

32 files changed

+1471
-371
lines changed

.beads/.gitignore

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ daemon.log
1111
daemon.pid
1212
bd.sock
1313
sync-state.json
14+
last-touched
1415

1516
# Local version tracking (prevents upgrade notification spam after git ops)
1617
.local_version
@@ -19,6 +20,10 @@ sync-state.json
1920
db.sqlite
2021
bd.db
2122

23+
# Worktree redirect file (contains relative path to main repo's .beads/)
24+
# Must not be committed as paths would be wrong in other clones
25+
redirect
26+
2227
# Merge artifacts (temporary files from 3-way merge)
2328
beads.base.jsonl
2429
beads.base.meta.json
@@ -27,8 +32,8 @@ beads.left.meta.json
2732
beads.right.jsonl
2833
beads.right.meta.json
2934

30-
# Keep JSONL exports and config (source of truth for git)
31-
!issues.jsonl
32-
!interactions.jsonl
33-
!metadata.json
34-
!config.json
35+
# NOTE: Do NOT add negation patterns (e.g., !issues.jsonl) here.
36+
# They would override fork protection in .git/info/exclude, allowing
37+
# contributors to accidentally commit upstream issue databases.
38+
# The JSONL files (issues.jsonl, interactions.jsonl) and config files
39+
# are tracked by git by default since no pattern above ignores them.

.beads/last-touched

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
Grace-4g4
1+
Grace-far

.grace/graceconfig.json

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{
2+
"OwnerName": "",
3+
"OrganizationName": "",
4+
"RepositoryName": "",
5+
"BranchName": "",
6+
"DefaultBranchName": "",
7+
"Themes": [
8+
{
9+
"Name": "Default",
10+
"DisplayColorOptions": {
11+
"added": "#00AF5F",
12+
"changed": "#800080",
13+
"deemphasized": "#808080",
14+
"deleted": "#8B0000",
15+
"error": "#FF0000",
16+
"highlighted": "#FFFFFF",
17+
"important": "#E5C07B",
18+
"verbose": "#FF7E00"
19+
}
20+
}
21+
],
22+
"LineEndings": "PlatformDependent",
23+
"Prefetch": [
24+
""
25+
],
26+
"RootDirectory": "D:\\Source\\Grace",
27+
"StandardizedRootDirectory": "D:/Source/Grace",
28+
"GraceDirectory": "D:\\Source\\Grace",
29+
"ObjectDirectory": "D:\\Source\\Grace",
30+
"GraceStatusFile": "gracestatus.msgpack",
31+
"GraceObjectCacheFile": "graceObjectCache.msgpack",
32+
"DirectoryVersionCache": "D:\\Source\\Grace",
33+
"ConfigurationDirectory": "D:\\Source\\Grace",
34+
"ObjectStorageProvider": "unknown",
35+
"ServerUri": "http://127.0.0.1:5000",
36+
"ProgramVersion": "0.1",
37+
"ConfigurationVersion": ""
38+
}

AGENTS.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
# Agent Instructions
22

3-
Other `AGENTS.md` files exist in subdirectories, refer to them for more specific context.
3+
Other `AGENTS.md` files exist in subdirectories, refer to them for more specific
4+
context.
45

56
## Issue Tracking
67

78
This project uses **bd (beads)** for issue tracking.
9+
Always use `bd` commands to manage your work.
810
Run `bd prime` for workflow context, or install hooks (`bd hooks install`) for auto-injection.
911

1012
**Quick reference:**

src/Grace.Actors/Constants.Actor.fs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,9 @@ module Constants =
116116
[<Literal>]
117117
let Branch = "Branch"
118118

119+
[<Literal>]
120+
let ConflictReceipt = "ConflictReceipt"
121+
119122
[<Literal>]
120123
let Diff = "Diff"
121124

@@ -129,16 +132,13 @@ module Constants =
129132
let FileAppearance = "FileApp"
130133

131134
[<Literal>]
132-
let NamedSection = "NamedSection"
135+
let GateAttestation = "GateAttestation"
133136

134137
[<Literal>]
135138
let IntegrationCandidate = "IntegrationCandidate"
136139

137140
[<Literal>]
138-
let GateAttestation = "GateAttestation"
139-
140-
[<Literal>]
141-
let ConflictReceipt = "ConflictReceipt"
141+
let NamedSection = "NamedSection"
142142

143143
[<Literal>]
144144
let Organization = "Organization"

src/Grace.Actors/DirectoryVersion.Actor.fs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -518,8 +518,9 @@ module DirectoryVersion =
518518
tags.Add("RecursiveSize", $"{directoryVersionDto.RecursiveSize}")
519519

520520
// Write the JSON using MessagePack serialization for efficiency.
521-
let blockBlobOpenWriteOptions = BlockBlobOpenWriteOptions(Tags = tags)
522-
blockBlobOpenWriteOptions.HttpHeaders.ContentType <- "application/msgpack"
521+
let blockBlobOpenWriteOptions =
522+
BlockBlobOpenWriteOptions(Tags = tags, HttpHeaders = BlobHttpHeaders(ContentType = "application/msgpack"))
523+
523524
use! blobStream = directoryVersionBlobClient.OpenWriteAsync(overwrite = true, options = blockBlobOpenWriteOptions)
524525
do! MessagePackSerializer.SerializeAsync(blobStream, subdirectoryVersionsList, messagePackSerializerOptions)
525526
do! blobStream.DisposeAsync()

src/Grace.Aspire.AppHost/Program.Aspire.AppHost.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,9 @@ private static void Main(string[] args)
332332
.WithEnvironment(EnvironmentVariables.AzureServiceBusTopic, configuration["Grace:ServiceBus:TopicName"] ?? "graceeventstream")
333333
.WithEnvironment(EnvironmentVariables.AzureServiceBusSubscription, configuration["Grace:ServiceBus:SubscriptionName"] ?? "grace-server")
334334
.WithEnvironment(EnvironmentVariables.GraceLogDirectory, publishLogDirectory)
335+
.WithEnvironment(EnvironmentVariables.GraceAuthOidcAuthority, configuration[EnvironmentVariables.GraceAuthOidcAuthority])
336+
.WithEnvironment(EnvironmentVariables.GraceAuthOidcAudience, configuration[EnvironmentVariables.GraceAuthOidcAudience])
337+
.WithEnvironment(EnvironmentVariables.GraceAuthOidcCliClientId, configuration[EnvironmentVariables.GraceAuthOidcCliClientId])
335338
.WithHttpEndpoint(targetPort: 5000, name: "http")
336339
.WithHttpsEndpoint(targetPort: 5001, name: "https")
337340
.AsHttp2Service()

src/Grace.CLI.Tests/Auth.Tests.fs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ module AuthTests =
2626
withEnv Constants.EnvironmentVariables.GraceAuthOidcAudience None (fun () ->
2727
withEnv Constants.EnvironmentVariables.GraceAuthOidcCliClientId None (fun () ->
2828
withEnv Constants.EnvironmentVariables.GraceAuthOidcM2mClientId None (fun () ->
29-
withEnv Constants.EnvironmentVariables.GraceAuthOidcM2mClientSecret None action))))
29+
withEnv Constants.EnvironmentVariables.GraceAuthOidcM2mClientSecret None (fun () ->
30+
withEnv Constants.EnvironmentVariables.GraceServerUri None action)))))
3031

3132
[<Test>]
3233
let ``tryGetAccessToken returns Error when auth is not configured`` () =
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
namespace Grace.CLI.Tests
2+
3+
open FsUnit
4+
open Grace.CLI
5+
open NUnit.Framework
6+
7+
[<TestFixture>]
8+
module CommandParsingTests =
9+
[<Test>]
10+
let ``top level command returns none for empty args`` () =
11+
GraceCommand.tryGetTopLevelCommandFromArgs Array.empty true |> should equal None
12+
13+
[<Test>]
14+
let ``top level command detects command token`` () =
15+
GraceCommand.tryGetTopLevelCommandFromArgs [| "connect"; "owner/org/repo" |] true
16+
|> should equal (Some "connect")
17+
18+
[<Test>]
19+
let ``top level command skips output option`` () =
20+
GraceCommand.tryGetTopLevelCommandFromArgs [| "--output"; "Verbose"; "connect" |] true
21+
|> should equal (Some "connect")
22+
23+
[<Test>]
24+
let ``top level command skips correlation id option`` () =
25+
GraceCommand.tryGetTopLevelCommandFromArgs [| "-c"; "abc123"; "connect" |] true
26+
|> should equal (Some "connect")
27+
28+
[<Test>]
29+
let ``top level command honors end of options marker`` () =
30+
GraceCommand.tryGetTopLevelCommandFromArgs [| "--output"; "Verbose"; "--"; "connect" |] true
31+
|> should equal (Some "connect")
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
namespace Grace.CLI.Tests
2+
3+
open FsUnit
4+
open Grace.CLI
5+
open Grace.CLI.Command
6+
open Grace.CLI.Text
7+
open Grace.Shared.Client.Configuration
8+
open Grace.Types.Branch
9+
open Grace.Types.Reference
10+
open Grace.Types.Types
11+
open NUnit.Framework
12+
open Spectre.Console
13+
open System
14+
open System.IO
15+
16+
[<NonParallelizable>]
17+
module ConnectTests =
18+
let private setAnsiConsoleOutput (writer: TextWriter) =
19+
let settings = AnsiConsoleSettings()
20+
settings.Out <- AnsiConsoleOutput(writer)
21+
AnsiConsole.Console <- AnsiConsole.Create(settings)
22+
let private runWithCapturedOutput (args: string array) =
23+
use writer = new StringWriter()
24+
let originalOut = Console.Out
25+
26+
try
27+
Console.SetOut(writer)
28+
setAnsiConsoleOutput writer
29+
let exitCode = GraceCommand.main args
30+
exitCode, writer.ToString()
31+
finally
32+
Console.SetOut(originalOut)
33+
setAnsiConsoleOutput originalOut
34+
35+
let private withTempDir (action: string -> unit) =
36+
let tempDir = Path.Combine(Path.GetTempPath(), $"grace-cli-tests-{Guid.NewGuid():N}")
37+
Directory.CreateDirectory(tempDir) |> ignore
38+
let originalDir = Environment.CurrentDirectory
39+
40+
try
41+
Environment.CurrentDirectory <- tempDir
42+
action tempDir
43+
finally
44+
Environment.CurrentDirectory <- originalDir
45+
46+
if Directory.Exists(tempDir) then
47+
try
48+
Directory.Delete(tempDir, true)
49+
with _ ->
50+
()
51+
52+
let private getGraceConfigPath root = Path.Combine(root, ".grace", "graceconfig.json")
53+
54+
[<Test>]
55+
let ``connect creates config when missing`` () =
56+
withTempDir (fun root ->
57+
let exitCode, _ = runWithCapturedOutput [| "connect" |]
58+
exitCode |> should equal -1
59+
File.Exists(getGraceConfigPath root) |> should equal true)
60+
61+
[<Test>]
62+
let ``connect retrieve default branch defaults to true`` () =
63+
let parseResult = GraceCommand.rootCommand.Parse([| "connect" |])
64+
65+
parseResult.GetValue<bool>(OptionName.RetrieveDefaultBranch)
66+
|> should equal true
67+
68+
[<Test>]
69+
let ``connect retrieve default branch parses explicit false`` () =
70+
let parseResult = GraceCommand.rootCommand.Parse([| "connect"; OptionName.RetrieveDefaultBranch; "false" |])
71+
72+
parseResult.GetValue<bool>(OptionName.RetrieveDefaultBranch)
73+
|> should equal false
74+
75+
[<Test>]
76+
let ``connect directory version selection precedence uses directory version id`` () =
77+
let directoryVersionId = Guid.NewGuid()
78+
let referenceId = Guid.NewGuid()
79+
80+
let parseResult =
81+
GraceCommand.rootCommand.Parse(
82+
[| "connect"
83+
OptionName.DirectoryVersionId
84+
$"{directoryVersionId}"
85+
OptionName.ReferenceId
86+
$"{referenceId}"
87+
OptionName.ReferenceType
88+
"Commit" |]
89+
)
90+
91+
match Connect.getDirectoryVersionSelection parseResult with
92+
| Connect.UseDirectoryVersionId selected -> selected |> should equal directoryVersionId
93+
| other -> Assert.Fail($"Unexpected selection: {other}")
94+
95+
[<Test>]
96+
let ``connect default directory version falls back to based-on`` () =
97+
let basedOnId = Guid.NewGuid()
98+
let branchDto = { BranchDto.Default with LatestPromotion = ReferenceDto.Default; BasedOn = { ReferenceDto.Default with DirectoryId = basedOnId } }
99+
100+
Connect.resolveDefaultDirectoryVersionId branchDto
101+
|> should equal (Some basedOnId)
102+
103+
[<Test>]
104+
let ``connect repository shortcut populates owner organization repository`` () =
105+
let parseResult = GraceCommand.rootCommand.Parse([| "connect"; "owner/org/repo" |])
106+
let graceIds = GraceIds.Default
107+
108+
match Connect.applyRepositoryShortcut parseResult graceIds with
109+
| Ok updated ->
110+
updated.OwnerName |> should equal "owner"
111+
updated.OrganizationName |> should equal "org"
112+
updated.RepositoryName |> should equal "repo"
113+
updated.OwnerId |> should equal Guid.Empty
114+
updated.OrganizationId |> should equal Guid.Empty
115+
updated.RepositoryId |> should equal Guid.Empty
116+
updated.HasOwner |> should equal true
117+
updated.HasOrganization |> should equal true
118+
updated.HasRepository |> should equal true
119+
| Error error -> Assert.Fail($"Unexpected error: {error.Error}")
120+
121+
[<Test>]
122+
let ``connect repository shortcut rejects missing segments`` () =
123+
let parseResult = GraceCommand.rootCommand.Parse([| "connect"; "owner/repo" |])
124+
let graceIds = GraceIds.Default
125+
126+
match Connect.applyRepositoryShortcut parseResult graceIds with
127+
| Ok _ -> Assert.Fail("Expected error when repository shortcut is missing segments.")
128+
| Error error -> error.Error |> should contain "owner/organization/repository"
129+
130+
[<Test>]
131+
let ``connect repository shortcut conflicts with explicit options`` () =
132+
let parseResult = GraceCommand.rootCommand.Parse([| "connect"; "owner/org/repo"; OptionName.OwnerName; "explicit-owner" |])
133+
134+
let graceIds = GraceIds.Default
135+
136+
match Connect.applyRepositoryShortcut parseResult graceIds with
137+
| Ok _ -> Assert.Fail("Expected error when shortcut is combined with explicit options.")
138+
| Error error -> error.Error |> should contain "Provide either the repository shortcut"

0 commit comments

Comments
 (0)