diff --git a/cmd/engram/main.go b/cmd/engram/main.go index c9243585..c7443911 100644 --- a/cmd/engram/main.go +++ b/cmd/engram/main.go @@ -812,14 +812,17 @@ func tryStartAutosync(ctx context.Context, s *store.Store, cfg store.Config) (au token := strings.TrimSpace(cc.Token) serverURL := strings.TrimSpace(cc.ServerURL) - // REQ-211: token required. + // REQ-211: token required. The token is resolved from cloud.json first and + // overridden by ENGRAM_CLOUD_TOKEN when set, so both sources are tried. + // On Windows (Task Scheduler), the env var is often absent — the file path + // is the expected source (issue #421). if token == "" { - log.Printf("[autosync] ERROR: ENGRAM_CLOUD_TOKEN is required when ENGRAM_CLOUD_AUTOSYNC=1; autosync disabled") + log.Printf("[autosync] ERROR: cloud token is not configured (set ENGRAM_CLOUD_TOKEN or store token in cloud.json via `engram cloud config`); autosync disabled") return nil, nil } - // REQ-211: server URL required. + // REQ-211: server URL required. Resolved from cloud.json or ENGRAM_CLOUD_SERVER. if serverURL == "" { - log.Printf("[autosync] ERROR: ENGRAM_CLOUD_SERVER is required when ENGRAM_CLOUD_AUTOSYNC=1; autosync disabled") + log.Printf("[autosync] ERROR: cloud server URL is not configured (set ENGRAM_CLOUD_SERVER or run `engram cloud config --server `); autosync disabled") return nil, nil } diff --git a/cmd/engram/sync_cloud_auth_test.go b/cmd/engram/sync_cloud_auth_test.go index bcaddf8c..7455b878 100644 --- a/cmd/engram/sync_cloud_auth_test.go +++ b/cmd/engram/sync_cloud_auth_test.go @@ -1,11 +1,13 @@ package main import ( + "context" "net/http" "net/http/httptest" "strings" "testing" + "github.com/Gentleman-Programming/engram/internal/cloud/autosync" "github.com/Gentleman-Programming/engram/internal/store" ) @@ -119,3 +121,56 @@ func TestSyncCloudSendsAuthorizationHeaderFromFileToken(t *testing.T) { t.Fatalf("expected Authorization header %q, got %q (file token not forwarded)", wantAuth, gotAuth) } } + +// TestTryStartAutosyncUsesFileToken asserts that tryStartAutosync picks up the +// cloud token from cloud.json when ENGRAM_CLOUD_TOKEN env var is absent (issue #421). +// This is the Windows Task Scheduler scenario: the background process runs in a +// separate session context without the env var, so the token must come from the +// persisted config file. +func TestTryStartAutosyncUsesFileToken(t *testing.T) { + cfg := testConfig(t) + t.Setenv("ENGRAM_CLOUD_AUTOSYNC", "1") + t.Setenv("ENGRAM_CLOUD_TOKEN", "") // env var absent — must fall back to file + t.Setenv("ENGRAM_CLOUD_SERVER", "") // env var absent — server from file too + + const fileToken = "file-only-token-421" + if err := saveCloudConfig(cfg, &cloudConfig{ + ServerURL: "http://127.0.0.1:19998", + Token: fileToken, + }); err != nil { + t.Fatalf("save cloud config: %v", err) + } + + s, err := store.New(cfg) + if err != nil { + t.Fatalf("store.New: %v", err) + } + defer s.Close() + + // Track whether the manager factory was reached. + managerCreated := false + old := newAutosyncManager + newAutosyncManager = func(_ *store.Store, _ autosync.CloudTransport, _ autosync.Config) startableAutosyncManager { + managerCreated = true + return &fakeStartableManager{} + } + defer func() { newAutosyncManager = old }() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + mgr, stopFn := tryStartAutosync(ctx, s, cfg) + + // If the file-token fallback is missing, tryStartAutosync returns (nil, nil) + // because cc.Token is empty after resolveCloudRuntimeConfig ignores the file. + if mgr == nil { + t.Fatal("tryStartAutosync returned nil manager when token is only in cloud.json — file token fallback not working for autosync startup (issue #421)") + } + if stopFn == nil { + t.Fatal("expected non-nil stop function when manager starts successfully") + } + if !managerCreated { + t.Fatal("newAutosyncManager factory was never reached — tryStartAutosync aborted before creating manager") + } + stopFn() +} diff --git a/plugin/claude-code/scripts/user-prompt-submit.ps1 b/plugin/claude-code/scripts/user-prompt-submit.ps1 index 87cef8a9..c5405b96 100644 --- a/plugin/claude-code/scripts/user-prompt-submit.ps1 +++ b/plugin/claude-code/scripts/user-prompt-submit.ps1 @@ -5,6 +5,13 @@ # fork emulation is slowed or blocked by Defender/EDR. Keep this script small # and dependency-free; it must never block prompt submission. +# Ensure UTF-8 output so JSON payloads with non-ASCII characters are not +# mangled when Claude Code reads this hook's stdout. Without this, Windows +# defaults to the system codepage (e.g. CP1252/CP850) which corrupts +# multi-byte characters in the systemMessage JSON (issue #421). +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 +[Console]::InputEncoding = [System.Text.Encoding]::UTF8 + $ErrorActionPreference = 'SilentlyContinue' function Write-EmptyHookResponse {