Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
117 commits
Select commit Hold shift + click to select a range
47e37bf
refactor: extract activation trace package
Grivn Jun 29, 2026
905418b
refactor: extract managed drive package
Grivn Jun 29, 2026
e8a5c1b
refactor: move multica registry to surface package
Grivn Jun 29, 2026
99cb49e
refactor: move multica hub metadata to surface package
Grivn Jun 29, 2026
7763d6d
refactor: introduce interaction event material
Grivn Jun 29, 2026
6da9184
feat: add harness product config model
Grivn Jun 29, 2026
a809496
feat: add harness daemon supervisor boundary
Grivn Jun 29, 2026
7a219cc
feat: add harness config and daemon commands
Grivn Jun 29, 2026
2cd2cff
fix: hide multica debug bridge commands
Grivn Jun 29, 2026
58d467d
feat: add harness agent and connect commands
Grivn Jun 29, 2026
271fcf7
refactor: extract projection comment material
Grivn Jun 29, 2026
eaa6355
feat: add acceptance multica provision entrypoint
Grivn Jun 29, 2026
7413650
test: classify r2 harness architecture packages
Grivn Jun 29, 2026
0a6d9dd
refactor: move multica projection surface policy
Grivn Jun 29, 2026
72cc3ad
fix: harden multica runtime dispatch entry
Grivn Jun 29, 2026
97ddd2e
refactor: structure multica runtime metadata projection
Grivn Jun 29, 2026
00339ed
refactor: centralize multica status projection
Grivn Jun 29, 2026
d038bc7
test: report multica acceptance prerequisites
Grivn Jun 29, 2026
7e35a16
feat: bridge multica registry into harness config
Grivn Jun 29, 2026
882468e
feat: show harness daemon role status
Grivn Jun 29, 2026
4151b30
test: guard r2 projection role boundaries
Grivn Jun 29, 2026
5e324b6
feat: add daemon worker state primitives
Grivn Jun 29, 2026
df532c5
refactor: move runtime comment projection to multica surface
Grivn Jun 29, 2026
568167a
refactor: move multica issue intake material
Grivn Jun 29, 2026
ed41bae
refactor: move runtime item projection to multica surface
Grivn Jun 29, 2026
5196a8c
refactor: move managed wake selection to drive
Grivn Jun 29, 2026
8c61386
refactor: centralize multica metadata merge
Grivn Jun 29, 2026
e3817a0
feat: persist harness daemon status snapshots
Grivn Jun 29, 2026
925a0c3
feat: add daemon work ledger state
Grivn Jun 29, 2026
8854802
docs: describe harness as event-system surface
Grivn Jun 29, 2026
0285f78
feat: add harness doctor command
Grivn Jun 29, 2026
acf4f04
feat: add harness session surface
Grivn Jun 29, 2026
dfe20d3
docs: clarify daemon and hub service roles
Grivn Jun 29, 2026
1c0e6e2
fix: keep multica routing metadata out of visible text
Grivn Jun 29, 2026
31c5123
refactor: add participant identity boundary
Grivn Jun 29, 2026
964b074
refactor: move multica issue identity parsing to surface
Grivn Jun 29, 2026
8a5f137
refactor: move multica hub defaults to surface
Grivn Jun 29, 2026
6736cfe
refactor: build multica root metadata in surface
Grivn Jun 29, 2026
ae9e065
test: preflight github mesh rate limits
Grivn Jun 29, 2026
7b80812
refactor: move multica mailbox metadata grouping
Grivn Jun 29, 2026
120b2c1
refactor: move multica runtime protocol helpers
Grivn Jun 29, 2026
cb515ab
feat: hide harness debug commands from root help
Grivn Jun 29, 2026
d6b2c82
refactor: drop multica projection wrapper from driver
Grivn Jun 29, 2026
9c6c716
test: guard driver projection boundary
Grivn Jun 29, 2026
683bcbd
refactor: build multica issue signals in surface
Grivn Jun 29, 2026
b06f134
test: guard github mesh sync interval
Grivn Jun 29, 2026
3289c4e
feat: persist configured daemon worker snapshot
Grivn Jun 29, 2026
8de743f
feat: add structured mnemon comment markers
Grivn Jun 29, 2026
d6061cd
feat: hide harness token admin command
Grivn Jun 29, 2026
ae45d2f
fix: reject duplicate harness daemon roles
Grivn Jun 29, 2026
1ef89d5
fix: redact bearer tokens from activation traces
Grivn Jun 29, 2026
87f7f43
test: guard service command boundaries
Grivn Jun 29, 2026
6b5c524
fix: reject empty daemon carriers in config
Grivn Jun 29, 2026
375d5db
fix: clean multica visible projection text
Grivn Jun 29, 2026
50e4c8e
test: require structured multica child text
Grivn Jun 29, 2026
4aa1335
test: cover multica progress comment text
Grivn Jun 29, 2026
398466c
test: preflight github mesh repository access
Grivn Jun 29, 2026
5514dbb
fix: recover multica feedback targets from hub metadata
Grivn Jun 29, 2026
06a61a0
fix: dispatch multica assignment event metadata
Grivn Jun 29, 2026
e5211c7
fix: harden harness control observations
Grivn Jun 29, 2026
33888e3
fix: stabilize multica hub projection
Grivn Jun 29, 2026
ff75a69
fix: allow managed wake retries after failure
Grivn Jun 29, 2026
ff273be
test: stage multica hub-flow handoffs
Grivn Jun 29, 2026
8a503f5
fix: harden multica hub routing
Grivn Jun 29, 2026
633feaf
fix: stabilize github publication listing
Grivn Jun 29, 2026
2b01507
fix: reduce github publication polling
Grivn Jun 29, 2026
b91eb1b
fix: structure multica hub surface metadata
Grivn Jun 29, 2026
e2f0e53
test: add multica parallel poc task case
Grivn Jun 29, 2026
01505e7
test: guard root cli boundary
Grivn Jun 29, 2026
e075f3a
test: guard multica projection writer role
Grivn Jun 29, 2026
7636d07
test: gate multica provisioning behind acceptance
Grivn Jun 29, 2026
9c287b8
fix: keep mnemonhub out of projection surfaces
Grivn Jun 29, 2026
d688cf2
fix: keep mnemonhub out of activation carriers
Grivn Jun 29, 2026
140722d
fix: parse structured multica issue input
Grivn Jun 29, 2026
17ab57e
refactor: move multica runtime config policy
Grivn Jun 29, 2026
5a7ad56
refactor: move multica runtime result text
Grivn Jun 29, 2026
6b0e532
refactor: move multica managed wake policy
Grivn Jun 29, 2026
c4b2375
refactor: move multica scope matching policy
Grivn Jun 29, 2026
1b057c6
refactor: move multica view item parsing
Grivn Jun 29, 2026
5ea9d52
refactor: move multica view traversal
Grivn Jun 29, 2026
2366bb2
refactor: move multica assignment projection metadata
Grivn Jun 29, 2026
bea2f42
refactor: move multica progress projection metadata
Grivn Jun 29, 2026
3d50f61
refactor: move multica assignment target selection
Grivn Jun 29, 2026
81db85a
refactor: move multica assignment mailbox matching
Grivn Jun 29, 2026
42c3026
refactor: move multica assignment mailbox material
Grivn Jun 29, 2026
bb7c0a0
refactor: move multica listed metadata parsing
Grivn Jun 29, 2026
7591dc4
refactor: move multica runtime path policy
Grivn Jun 29, 2026
cd8451c
refactor: centralize multica issue hub metadata lookup
Grivn Jun 29, 2026
d5d279b
refactor: centralize multica issue metadata loading
Grivn Jun 29, 2026
4970990
refactor: reuse multica registry selection in acceptance
Grivn Jun 29, 2026
5e815a0
refactor: reuse multica metadata resolver in acceptance
Grivn Jun 29, 2026
d00e736
refactor: centralize multica runtime command name
Grivn Jun 29, 2026
dff95cb
refactor: centralize multica runtime profile name
Grivn Jun 29, 2026
d5f5140
fix: keep mnemonhub help on successful path
Grivn Jun 29, 2026
fce8b55
test: cover manual sync through mnemonhub
Grivn Jun 29, 2026
2d17e0c
test: preserve partial multica hub evidence
Grivn Jun 29, 2026
213674d
fix: let acceptance interrupts write reports
Grivn Jun 29, 2026
7683fe6
test: keep mnemonhub daemon role as exchange
Grivn Jun 29, 2026
75db5fc
fix: absolutize multica participant env paths
Grivn Jun 29, 2026
d438a15
refactor: move daemon role snapshots internal
Grivn Jun 29, 2026
12bc619
test: guard root cli cluster surface names
Grivn Jun 29, 2026
7abe936
test: keep harness internals hidden
Grivn Jun 29, 2026
d74d6a4
fix: name multica runtime progress as adapter
Grivn Jun 29, 2026
352ee69
fix: hide legacy harness service verbs
Grivn Jun 29, 2026
8347506
test: pin harness public command surface
Grivn Jun 29, 2026
df99daf
fix: let mnemond help exit cleanly
Grivn Jun 29, 2026
f15dab2
fix: describe multica runtime adapter help
Grivn Jun 29, 2026
634baef
fix: show mnemond service command help
Grivn Jun 29, 2026
f8beb86
fix: show mnemon-hub serve help
Grivn Jun 29, 2026
f0224cc
feat: add mnemond readiness doctor
Grivn Jun 29, 2026
8e34b48
fix: normalize mnemond lifecycle help
Grivn Jun 29, 2026
e9a4f02
refactor: structure multica runtime activation context
Grivn Jun 29, 2026
518bd30
fix: reject occupied mnemond listen addresses
Grivn Jun 29, 2026
ad6181a
test: guard multica hub-flow prerequisites
Grivn Jun 29, 2026
2f39092
fix: pass managed wake context to codex appserver
Grivn Jun 29, 2026
8ecac01
feat: show structured daemon role details
Grivn Jun 29, 2026
71eca53
chore: keep mnemon dev notes untracked
Grivn Jun 30, 2026
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
/mnemon-harness
/mnemon-hub
/mnemond
/mnemon-multica-runtime
/mnemon-acceptance
/bin/

# Local dogfood / capability test sandboxes (per-test subdirs)
Expand Down
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,11 @@ deps: ## Download Go dependencies
build: ## Build the mnemon binary
go build -ldflags "$(LDFLAGS)" -o $(BINARY) .

harness-build: ## Build the harness binaries (mnemon-harness local plane + mnemon-hub remote hub + mnemond local governance daemon + test-only acceptance runner)
harness-build: ## Build the harness binaries (mnemon-harness, mnemond, mnemonhub, Multica runtime adapter, and test-only acceptance runner)
go build -ldflags "$(LDFLAGS)" -o mnemon-harness ./harness/cmd/mnemon-harness
go build -ldflags "$(LDFLAGS)" -o mnemon-hub ./harness/cmd/mnemon-hub
go build -ldflags "$(LDFLAGS)" -o mnemond ./harness/cmd/mnemond
go build -ldflags "$(LDFLAGS)" -o mnemon-multica-runtime ./harness/cmd/mnemon-multica-runtime
go build -ldflags "$(LDFLAGS)" -o mnemon-acceptance ./harness/cmd/mnemon-acceptance

# ── Install / Uninstall ─────────────────────────────────────────────
Expand Down
175 changes: 173 additions & 2 deletions harness/cmd/mnemon-acceptance/acceptance_github_mesh.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"sort"
Expand All @@ -30,6 +31,12 @@ var (
acceptanceGitHubSyncInterval time.Duration
)

var (
r1GitHubMeshRateLimitAPIURL = "https://api.github.com/rate_limit"
r1GitHubMeshAPIBaseURL = "https://api.github.com"
r1GitHubMeshHTTPClient = &http.Client{Timeout: 10 * time.Second}
)

var acceptanceR1GitHubMeshCmd = &cobra.Command{
Use: "r1-github-mesh-task-suite",
Short: "Run GitHub-backed Remote Workspace task-suite acceptance",
Expand Down Expand Up @@ -155,6 +162,11 @@ func runR1GitHubMeshAcceptance(ctx context.Context, opts r1GitHubMeshAcceptanceO
report.Status = "blocked"
return report, err
}
if err := validateR1GitHubMeshSyncInterval(opts); err != nil {
addR1Error(&report, err)
report.Status = "blocked"
return report, err
}
if opts.TokenFile == "" {
err := fmt.Errorf("--github-token-file is required")
addR1Error(&report, err)
Expand All @@ -173,6 +185,24 @@ func runR1GitHubMeshAcceptance(ctx context.Context, opts r1GitHubMeshAcceptanceO
report.Status = "blocked"
return report, err
}
report.Artifacts["github_repo"] = opts.Repo
report.Artifacts["github_token_file"] = tokenFile
rateLimit, err := preflightR1GitHubMeshRateLimit(ctx, tokenFile, r1GitHubMeshMinimumRateLimitRemaining(opts))
if rateLimit.Limit > 0 || !rateLimit.ResetAt.IsZero() {
report.Artifacts["github_rate_limit_remaining"] = fmt.Sprintf("%d", rateLimit.Remaining)
report.Artifacts["github_rate_limit_limit"] = fmt.Sprintf("%d", rateLimit.Limit)
report.Artifacts["github_rate_limit_reset"] = rateLimit.ResetAt.UTC().Format(time.RFC3339)
}
if err != nil {
addR1Error(&report, err)
report.Status = "blocked"
return report, err
}
if err := preflightR1GitHubMeshRepositoryAccess(ctx, opts.Repo, tokenFile); err != nil {
addR1Error(&report, err)
report.Status = "blocked"
return report, err
}
branches := r1GitHubMeshBranches(branchPrefix, opts.Agents)
if err := ensureR1GitHubMeshBranches(ctx, opts.Repo, tokenFile, branches); err != nil {
addR1Error(&report, err)
Expand All @@ -187,8 +217,6 @@ func runR1GitHubMeshAcceptance(ctx context.Context, opts r1GitHubMeshAcceptanceO
}
sourceCodexHome := resolveSourceCodexHome(opts.CodexHome)
report.Artifacts["codex_home_source"] = sourceCodexHome
report.Artifacts["github_repo"] = opts.Repo
report.Artifacts["github_token_file"] = tokenFile
report.Artifacts["github_branch_prefix"] = branchPrefix
report.Artifacts["github_sync_interval"] = opts.SyncInterval.String()
initialOnline := opts.Agents
Expand Down Expand Up @@ -1383,6 +1411,16 @@ func r1GitHubMeshBranchPrefix(prefix string, started time.Time) string {
return "mnemon/mnemond-" + started.UTC().Format("20060102T150405Z") + "-"
}

func validateR1GitHubMeshSyncInterval(opts r1GitHubMeshAcceptanceOptions) error {
if !opts.AgentTurns {
return nil
}
if opts.SyncInterval < 30*time.Second {
return fmt.Errorf("github mesh agent-turns require --sync-interval >= 30s to protect GitHub API quota (got %s)", opts.SyncInterval)
}
return nil
}

func ensureR1GitHubMeshBranches(ctx context.Context, repo, tokenFile string, branches []string) error {
token, err := readR1GitHubMeshToken(tokenFile)
if err != nil {
Expand All @@ -1402,6 +1440,139 @@ func ensureR1GitHubMeshBranches(ctx context.Context, repo, tokenFile string, bra
return nil
}

type r1GitHubMeshRateLimit struct {
Limit int
Remaining int
Used int
ResetAt time.Time
}

func r1GitHubMeshMinimumRateLimitRemaining(opts r1GitHubMeshAcceptanceOptions) int {
scenarios := len(r1GitHubMeshScenarioNames(opts.Scenarios))
if scenarios == 0 {
scenarios = 1
}
agents := opts.Agents
if agents < 5 {
agents = 5
}
min := 500 + agents*scenarios*150
if min < 1500 {
return 1500
}
return min
}

func preflightR1GitHubMeshRateLimit(ctx context.Context, tokenFile string, minRemaining int) (r1GitHubMeshRateLimit, error) {
token, err := readR1GitHubMeshToken(tokenFile)
if err != nil {
return r1GitHubMeshRateLimit{}, err
}
limit, err := fetchR1GitHubMeshRateLimit(ctx, token)
if err != nil {
return r1GitHubMeshRateLimit{}, err
}
if limit.Remaining < minRemaining {
return limit, fmt.Errorf("github core API rate limit remaining %d below required %d; reset=%s", limit.Remaining, minRemaining, limit.ResetAt.UTC().Format(time.RFC3339))
}
return limit, nil
}

func preflightR1GitHubMeshRepositoryAccess(ctx context.Context, repo, tokenFile string) error {
token, err := readR1GitHubMeshToken(tokenFile)
if err != nil {
return err
}
return fetchR1GitHubMeshRepositoryAccess(ctx, repo, token)
}

func fetchR1GitHubMeshRepositoryAccess(ctx context.Context, repo, token string) error {
repo, err := exchange.NormalizeGitHubRepo(repo)
if err != nil {
return err
}
baseURL := strings.TrimRight(strings.TrimSpace(r1GitHubMeshAPIBaseURL), "/")
if baseURL == "" {
baseURL = "https://api.github.com"
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, baseURL+"/repos/"+repo, nil)
if err != nil {
return err
}
req.Header.Set("Accept", "application/vnd.github+json")
req.Header.Set("X-GitHub-Api-Version", "2022-11-28")
req.Header.Set("User-Agent", "mnemon-acceptance")
if strings.TrimSpace(token) != "" {
req.Header.Set("Authorization", "Bearer "+strings.TrimSpace(token))
}
client := r1GitHubMeshHTTPClient
if client == nil {
client = http.DefaultClient
}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
if resp.StatusCode >= 300 {
return fmt.Errorf("github repo access status %d for %s: %s", resp.StatusCode, repo, strings.TrimSpace(string(body)))
}
return nil
}

func fetchR1GitHubMeshRateLimit(ctx context.Context, token string) (r1GitHubMeshRateLimit, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, r1GitHubMeshRateLimitAPIURL, nil)
if err != nil {
return r1GitHubMeshRateLimit{}, err
}
req.Header.Set("Accept", "application/vnd.github+json")
req.Header.Set("X-GitHub-Api-Version", "2022-11-28")
req.Header.Set("User-Agent", "mnemon-acceptance")
if strings.TrimSpace(token) != "" {
req.Header.Set("Authorization", "Bearer "+strings.TrimSpace(token))
}
client := r1GitHubMeshHTTPClient
if client == nil {
client = http.DefaultClient
}
resp, err := client.Do(req)
if err != nil {
return r1GitHubMeshRateLimit{}, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return r1GitHubMeshRateLimit{}, err
}
if resp.StatusCode >= 300 {
return r1GitHubMeshRateLimit{}, fmt.Errorf("github rate_limit status %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
}
var doc struct {
Resources struct {
Core struct {
Limit int `json:"limit"`
Remaining int `json:"remaining"`
Reset int64 `json:"reset"`
Used int `json:"used"`
} `json:"core"`
} `json:"resources"`
}
if err := json.Unmarshal(body, &doc); err != nil {
return r1GitHubMeshRateLimit{}, fmt.Errorf("parse github rate_limit response: %w", err)
}
core := doc.Resources.Core
return r1GitHubMeshRateLimit{
Limit: core.Limit,
Remaining: core.Remaining,
Used: core.Used,
ResetAt: time.Unix(core.Reset, 0).UTC(),
}, nil
}

func readR1GitHubMeshToken(tokenFile string) (string, error) {
body, err := os.ReadFile(tokenFile)
if err != nil {
Expand Down
139 changes: 139 additions & 0 deletions harness/cmd/mnemon-acceptance/acceptance_github_mesh_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package main
import (
"context"
"database/sql"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
Expand Down Expand Up @@ -48,6 +50,143 @@ func TestR1GitHubMeshBranchPrefixDefaultsToRunScopedBranches(t *testing.T) {
}
}

func TestFetchR1GitHubMeshRateLimit(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if got := r.Header.Get("Authorization"); got != "Bearer secret-token" {
t.Fatalf("authorization header = %q", got)
}
_, _ = w.Write([]byte(`{"resources":{"core":{"limit":5000,"remaining":4321,"reset":1782725122,"used":679}}}`))
}))
defer server.Close()
oldURL := r1GitHubMeshRateLimitAPIURL
oldClient := r1GitHubMeshHTTPClient
r1GitHubMeshRateLimitAPIURL = server.URL
r1GitHubMeshHTTPClient = server.Client()
t.Cleanup(func() {
r1GitHubMeshRateLimitAPIURL = oldURL
r1GitHubMeshHTTPClient = oldClient
})

limit, err := fetchR1GitHubMeshRateLimit(context.Background(), "secret-token")
if err != nil {
t.Fatal(err)
}
if limit.Limit != 5000 || limit.Remaining != 4321 || limit.Used != 679 || limit.ResetAt.Unix() != 1782725122 {
t.Fatalf("rate limit mismatch: %+v", limit)
}
}

func TestPreflightR1GitHubMeshRateLimitBlocksLowRemaining(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(`{"resources":{"core":{"limit":5000,"remaining":42,"reset":1782725122,"used":4958}}}`))
}))
defer server.Close()
oldURL := r1GitHubMeshRateLimitAPIURL
oldClient := r1GitHubMeshHTTPClient
r1GitHubMeshRateLimitAPIURL = server.URL
r1GitHubMeshHTTPClient = server.Client()
t.Cleanup(func() {
r1GitHubMeshRateLimitAPIURL = oldURL
r1GitHubMeshHTTPClient = oldClient
})
tokenFile := filepath.Join(t.TempDir(), "github.token")
if err := os.WriteFile(tokenFile, []byte("secret-token\n"), 0o600); err != nil {
t.Fatal(err)
}
limit, err := preflightR1GitHubMeshRateLimit(context.Background(), tokenFile, 1500)
if err == nil || !strings.Contains(err.Error(), "below required 1500") {
t.Fatalf("expected low-rate-limit error, got limit=%+v err=%v", limit, err)
}
if limit.Remaining != 42 {
t.Fatalf("rate limit should be returned for diagnostics: %+v", limit)
}
}

func TestR1GitHubMeshAcceptanceBlocksInvalidRepositoryTokenBeforeSetup(t *testing.T) {
tmp := t.TempDir()
var repoChecked bool
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if got := r.Header.Get("Authorization"); got != "Bearer secret-token" {
t.Fatalf("authorization header = %q", got)
}
switch r.URL.Path {
case "/rate_limit":
_, _ = w.Write([]byte(`{"resources":{"core":{"limit":5000,"remaining":5000,"reset":1782725122,"used":0}}}`))
case "/repos/mnemon-dev/mnemon-teamwork-example":
repoChecked = true
w.WriteHeader(http.StatusUnauthorized)
_, _ = w.Write([]byte(`{"message":"Bad credentials"}`))
default:
t.Fatalf("unexpected path %s", r.URL.Path)
}
}))
defer server.Close()
oldRateLimitURL := r1GitHubMeshRateLimitAPIURL
oldAPIBaseURL := r1GitHubMeshAPIBaseURL
oldClient := r1GitHubMeshHTTPClient
r1GitHubMeshRateLimitAPIURL = server.URL + "/rate_limit"
r1GitHubMeshAPIBaseURL = server.URL
r1GitHubMeshHTTPClient = server.Client()
t.Cleanup(func() {
r1GitHubMeshRateLimitAPIURL = oldRateLimitURL
r1GitHubMeshAPIBaseURL = oldAPIBaseURL
r1GitHubMeshHTTPClient = oldClient
})

tokenFile := filepath.Join(tmp, "github.token")
if err := os.WriteFile(tokenFile, []byte("secret-token\n"), 0o600); err != nil {
t.Fatal(err)
}
report, err := runR1GitHubMeshAcceptance(context.Background(), r1GitHubMeshAcceptanceOptions{
r1CodexAcceptanceOptions: r1CodexAcceptanceOptions{
RunRoot: filepath.Join(tmp, "run"),
AgentTurns: true,
TurnTimeout: time.Millisecond,
},
Repo: "mnemon-dev/mnemon-teamwork-example",
TokenFile: tokenFile,
SyncInterval: 30 * time.Second,
})
if err == nil || !strings.Contains(err.Error(), "github repo access status 401") || !strings.Contains(err.Error(), "Bad credentials") {
t.Fatalf("expected invalid repo token blocker, got report=%+v err=%v", report, err)
}
if !repoChecked {
t.Fatal("repository access preflight was not called")
}
if report.Status != "blocked" || !strings.Contains(strings.Join(report.Errors, "\n"), "Bad credentials") {
t.Fatalf("report should be blocked with repo credential error: %+v", report)
}
if _, err := os.Stat(filepath.Join(report.RunRoot, "bin")); !os.IsNotExist(err) {
t.Fatalf("acceptance should block before installing binaries, stat err=%v", err)
}
data, err := os.ReadFile(report.ReportPath)
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(data), "github repo access status 401") {
t.Fatalf("written report missing repo access blocker:\n%s", data)
}
}

func TestValidateR1GitHubMeshSyncIntervalProtectsAgentTurns(t *testing.T) {
err := validateR1GitHubMeshSyncInterval(r1GitHubMeshAcceptanceOptions{
r1CodexAcceptanceOptions: r1CodexAcceptanceOptions{AgentTurns: true},
SyncInterval: 10 * time.Second,
})
if err == nil || !strings.Contains(err.Error(), ">= 30s") {
t.Fatalf("expected short sync interval guard, got %v", err)
}
if err := validateR1GitHubMeshSyncInterval(r1GitHubMeshAcceptanceOptions{
r1CodexAcceptanceOptions: r1CodexAcceptanceOptions{AgentTurns: true},
SyncInterval: 30 * time.Second,
}); err != nil {
t.Fatalf("30s sync interval should be allowed: %v", err)
}
if err := validateR1GitHubMeshSyncInterval(r1GitHubMeshAcceptanceOptions{SyncInterval: 10 * time.Second}); err != nil {
t.Fatalf("short non-agent-turn sync interval should be allowed: %v", err)
}
}

func TestWriteR1GitHubMeshRemotesCreatesPublishAndSubscribePlan(t *testing.T) {
root := t.TempDir()
tokenFile := filepath.Join(root, "github.token")
Expand Down
Loading
Loading