Skip to content
Merged
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
6 changes: 6 additions & 0 deletions docs/debug-command.md
Original file line number Diff line number Diff line change
Expand Up @@ -587,6 +587,12 @@ The `--format` flag accepts only `text` (default) or `json`. Any other value is
invalid --format "xml": must be 'text' or 'json'
```

### Registration summaries

`protocol:diagnose`, `protocol:status`, and `protocol:verify` now print a one-line **Summary** before detailed checks. The summary reflects the overall registration state (`ok`, `degraded`, `not_registered`, or `error`) and the number of detected issues.

`protocol:register` validates the current binary path and platform support **before** writing any OS registration artefacts. Invalid inputs fail immediately with actionable remediation text instead of low-level OS errors.

---

## Session Recovery and Integrity
Expand Down
6 changes: 5 additions & 1 deletion internal/cmd/protocol.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ To fix a broken registration automatically, use 'glassbox protocol:repair'.`,
}

diag := registrar.Diagnose()
fmt.Fprintf(cmd.OutOrStdout(), "Summary: %s\n", diag.Summary())
for _, check := range diag.Checks {
fmt.Fprintf(cmd.OutOrStdout(), "[OK] %s\n", check)
}
Expand Down Expand Up @@ -209,6 +210,7 @@ On failure, remediation steps are printed to help repair the registration.`,

if err != nil || !probePassed {
diag := registrar.Diagnose()
fmt.Fprintf(cmd.ErrOrStderr(), "\nSummary: %s\n", diag.Summary())
if len(diag.RemediationSteps) > 0 {
fmt.Fprintf(cmd.ErrOrStderr(), "\nRemediation steps:\n")
for i, step := range diag.RemediationSteps {
Expand All @@ -225,6 +227,7 @@ On failure, remediation steps are printed to help repair the registration.`,
}

fmt.Fprintf(cmd.OutOrStdout(), "Verified GLASSBOX Protocol registration on %s (%dms)\n", report.Platform, report.ElapsedMs)
fmt.Fprintf(cmd.OutOrStdout(), "Summary: %s\n", report.Summary())
return nil
},
}
Expand Down Expand Up @@ -372,7 +375,8 @@ Exit codes:
fmt.Fprintf(cmd.ErrOrStderr(), "[FAIL] %s\n", issue)
}

fmt.Fprintf(cmd.OutOrStdout(), "\nStatus: %s (platform: %s, scheme: %s://)\n",
fmt.Fprintf(cmd.OutOrStdout(), "\nSummary: %s\n", report.Summary())
fmt.Fprintf(cmd.OutOrStdout(), "Status: %s (platform: %s, scheme: %s://)\n",
report.Status, report.Platform, report.Scheme)

if report.RegisteredHandler != "" {
Expand Down
25 changes: 25 additions & 0 deletions internal/cmd/protocol_guidance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -216,3 +216,28 @@ func TestProtocolRegisterCmd_FailureError_MentionsRepairTip(t *testing.T) {
t.Errorf("register failure should mention protocol:repair; got: %s", wrapped)
}
}

// ── DiagnosticReport.Summary rendering ───────────────────────────────────────

func TestProtocolDiagnoseCmd_SummaryLine_IncludesStatus(t *testing.T) {
report := &protocolreg.DiagnosticReport{
Platform: "linux",
Scheme: protocolreg.Scheme,
Status: protocolreg.StatusDegraded,
Issues: []string{"wrapper script does not reference current binary"},
}

var stdout strings.Builder
stdout.WriteString("Summary: " + report.Summary() + "\n")

out := stdout.String()
if !strings.Contains(out, "Summary:") {
t.Errorf("diagnose output should include Summary line, got: %s", out)
}
if !strings.Contains(out, "degraded") {
t.Errorf("summary should mention degraded status, got: %s", out)
}
if !strings.Contains(out, protocolreg.Scheme+"://") {
t.Errorf("summary should include scheme, got: %s", out)
}
}
37 changes: 37 additions & 0 deletions internal/protocolreg/registration.go
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,43 @@ func (r *Registrar) Verify() (*VerificationReport, error) {
return report, nil
}

// validateRegistrationPreconditions rejects invalid inputs before any OS
// registration artefacts are written.
func (r *Registrar) validateRegistrationPreconditions() error {
if r.executablePath == "" {
return fmt.Errorf(
"cannot register: executable path is empty\n" +
" Fix: ensure glassbox is invoked from a valid binary path, not via 'go run'",
)
}

if strings.ContainsRune(r.executablePath, 0) {
return fmt.Errorf(
"cannot register: executable path contains a null byte\n" +
" Fix: reinstall Glassbox from a trusted source",
)
}

if _, err := os.Stat(r.executablePath); err != nil {
return fmt.Errorf(
"cannot register: executable not found at %s: %w\n"+
" Fix: reinstall Glassbox or verify the binary path is correct",
r.executablePath, err,
)
}

switch runtime.GOOS {
case "windows", "darwin", "linux":
return nil
default:
return fmt.Errorf(
"protocol registration is not supported on %s\n"+
" Fix: use Linux, macOS, or Windows to register the %s:// handler",
runtime.GOOS, Scheme,
)
}
}

func (r *Registrar) registerWindows() error {
// Detect Protocol Registry Conflicts (Issue #1198)
registryOutput, err := runCommand("reg", "query", windowsRegistryKey, "/ve")
Expand Down
2 changes: 0 additions & 2 deletions internal/protocolreg/registration_errors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -198,8 +198,6 @@ func TestShellQuote_EmptyString(t *testing.T) {
}
}

// ── XdgMime missing error message ────────────────────────────────────────────

func TestRegisterLinux_XdgMissingMessage_IsActionable(t *testing.T) {
// Construct the exact error message that registerLinux would return when
// xdg-mime is absent, and verify it contains installation guidance.
Expand Down
75 changes: 75 additions & 0 deletions internal/protocolreg/summary.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// Copyright 2026 Glassbox Users
// SPDX-License-Identifier: Apache-2.0

package protocolreg

import (
"fmt"
"strings"
)

// Summary returns a concise human-readable overview of the diagnostic report.
func (report *DiagnosticReport) Summary() string {
if report == nil {
return "Protocol registration diagnostics unavailable."
}

issueCount := len(report.Issues)
switch report.Status {
case StatusOK:
return fmt.Sprintf(
"Protocol handler %s:// is registered and healthy on %s.",
report.Scheme, report.Platform,
)
case StatusNotRegistered:
if issueCount == 0 {
return fmt.Sprintf(
"Protocol handler %s:// is not registered on %s.",
report.Scheme, report.Platform,
)
}
return fmt.Sprintf(
"Protocol handler %s:// is not registered on %s (%d issue(s)).",
report.Scheme, report.Platform, issueCount,
)
case StatusDegraded:
return fmt.Sprintf(
"Protocol handler %s:// is registered but degraded on %s (%d issue(s)).",
report.Scheme, report.Platform, issueCount,
)
case StatusError:
if issueCount == 0 {
return fmt.Sprintf(
"Protocol registration diagnostics failed on %s.",
report.Platform,
)
}
return fmt.Sprintf(
"Protocol registration diagnostics failed on %s: %s",
report.Platform, strings.Join(report.Issues, "; "),
)
default:
return fmt.Sprintf(
"Protocol handler %s:// status is %s on %s.",
report.Scheme, report.Status, report.Platform,
)
}
}

// Summary returns a concise human-readable overview of the verification report.
func (report *VerificationReport) Summary() string {
if report == nil {
return "Protocol registration verification unavailable."
}

if len(report.Issues) == 0 {
return fmt.Sprintf(
"Verified %s:// protocol registration on %s (%d check(s) passed).",
report.Scheme, report.Platform, len(report.Checks),
)
}
return fmt.Sprintf(
"Protocol verification failed on %s (%d issue(s), %d check(s) passed).",
report.Platform, len(report.Issues), len(report.Checks),
)
}
114 changes: 114 additions & 0 deletions internal/protocolreg/summary_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
// Copyright 2026 Glassbox Users
// SPDX-License-Identifier: Apache-2.0

package protocolreg

import (
"strings"
"testing"
)

func TestDiagnosticReport_Summary_OK(t *testing.T) {
report := &DiagnosticReport{
Platform: "linux",
Scheme: Scheme,
Status: StatusOK,
}
s := report.Summary()
if !strings.Contains(s, "healthy") {
t.Errorf("Summary() should mention healthy state, got: %q", s)
}
if !strings.Contains(s, Scheme+"://") {
t.Errorf("Summary() should include scheme, got: %q", s)
}
}

func TestDiagnosticReport_Summary_NotRegistered(t *testing.T) {
report := &DiagnosticReport{
Platform: "darwin",
Scheme: Scheme,
Status: StatusNotRegistered,
Issues: []string{"app bundle plist not found"},
}
s := report.Summary()
if !strings.Contains(s, "not registered") {
t.Errorf("Summary() should mention not registered, got: %q", s)
}
if !strings.Contains(s, "1 issue") {
t.Errorf("Summary() should include issue count, got: %q", s)
}
}

func TestDiagnosticReport_Summary_Degraded(t *testing.T) {
report := &DiagnosticReport{
Platform: "linux",
Scheme: Scheme,
Status: StatusDegraded,
Issues: []string{"stale path", "xdg-mime mismatch"},
}
s := report.Summary()
if !strings.Contains(s, "degraded") {
t.Errorf("Summary() should mention degraded, got: %q", s)
}
if !strings.Contains(s, "2 issue") {
t.Errorf("Summary() should include issue count, got: %q", s)
}
}

func TestDiagnosticReport_Summary_Error(t *testing.T) {
report := &DiagnosticReport{
Platform: "freebsd",
Scheme: Scheme,
Status: StatusError,
Issues: []string{"unsupported platform"},
}
s := report.Summary()
if !strings.Contains(s, "unsupported platform") {
t.Errorf("Summary() should include issue text, got: %q", s)
}
}

func TestDiagnosticReport_Summary_NilReport(t *testing.T) {
var report *DiagnosticReport
if s := report.Summary(); s == "" {
t.Error("Summary() on nil report should return a non-empty fallback")
}
}

func TestVerificationReport_Summary_Passed(t *testing.T) {
report := &VerificationReport{
Platform: "linux",
Scheme: Scheme,
Checks: []string{"desktop file found", "wrapper script ok"},
}
s := report.Summary()
if !strings.Contains(s, "Verified") {
t.Errorf("Summary() should mention verification success, got: %q", s)
}
if !strings.Contains(s, "2 check") {
t.Errorf("Summary() should include check count, got: %q", s)
}
}

func TestVerificationReport_Summary_Failed(t *testing.T) {
report := &VerificationReport{
Platform: "windows",
Scheme: Scheme,
Checks: []string{"registry key exists"},
Issues: []string{"missing URL Protocol value", "open command mismatch"},
}
s := report.Summary()
if !strings.Contains(s, "failed") {
t.Errorf("Summary() should mention failure, got: %q", s)
}
if !strings.Contains(s, "2 issue") {
t.Errorf("Summary() should include issue count, got: %q", s)
}
}

func TestVerificationReport_Summary_NilReport(t *testing.T) {
var report *VerificationReport
if s := report.Summary(); s == "" {
t.Error("Summary() on nil report should return a non-empty fallback")
}
}
Loading
Loading