diff --git a/internal/cli/orient_test.go b/internal/cli/orient_test.go index 6395a31..27e0989 100644 --- a/internal/cli/orient_test.go +++ b/internal/cli/orient_test.go @@ -267,6 +267,10 @@ func setupCLIDaybook(t *testing.T, notes ...cliNote) (string, string) { if writer == "" { t.Fatalf("publisher writer missing in %s", writers) } + code, body, _ = runMain(t, "", "get", "notes/missing.md", "--cortex", "vault") + if code != 1 || body["error"] != "not_found" { + t.Fatalf("missing get: exit=%d body=%v", code, body) + } return human, writer } diff --git a/internal/kernel/clean_writer_test.go b/internal/kernel/clean_writer_test.go index 58ee1bc..a151180 100644 --- a/internal/kernel/clean_writer_test.go +++ b/internal/kernel/clean_writer_test.go @@ -24,11 +24,6 @@ func (f *fixture) noOriginClone(t *testing.T, name string) string { return dir } -// Proof 15 (sole publisher): the kernel-owned writer clone is the sole -// publisher for daybook cortices. Writes land, commit, and push from -// the writer clone without touching the registered human checkout, -// even when the human checkout carries uncommitted edits, staged work, -// or a dirty heartbeat. Reads immediately resolve from the publisher clone. func TestProof15SolePublisherIsolatesFromHumanCheckout(t *testing.T) { f := newFixture(t) @@ -88,8 +83,6 @@ func TestProof15SolePublisherIsolatesFromHumanCheckout(t *testing.T) { } } -// A LEFTOVER-DIRTY PUBLISHER clone must abort the write at the publisher- -// root scan — the publisher repository must always be clean. func TestDirtyPublisherAborts(t *testing.T) { f := newFixture(t) @@ -118,8 +111,6 @@ func TestDirtyPublisherAborts(t *testing.T) { } } -// Proof 16: publisher pins the registered checkout's actual tracked branch -// (e.g. feature-vault), not the remote's default branch (master). func TestPublisherPinsNonDefaultTrackedBranch(t *testing.T) { f := newFixture(t) // Create and checkout non-default branch on hosta @@ -149,8 +140,6 @@ func TestPublisherPinsNonDefaultTrackedBranch(t *testing.T) { } } -// Proof 17: Get on a freshly registered cortex provisions the publisher clone -// and reads clean committed state, never uncommitted human dirt on c.Path. func TestProof17FreshRegistrationReadsCommittedStateNotHumanDirt(t *testing.T) { testConfigEnv(t) base := t.TempDir() @@ -199,8 +188,6 @@ func TestProof17FreshRegistrationReadsCommittedStateNotHumanDirt(t *testing.T) { } } -// Proof 18: provisioning failure fails closed — Get/Log/Lint return cortex_unavailable, -// never silently falling back to uncommitted bytes on c.Path. func TestProof18ProvisioningFailureFailsClosed(t *testing.T) { testConfigEnv(t) base := t.TempDir() @@ -233,9 +220,6 @@ func TestProof18ProvisioningFailureFailsClosed(t *testing.T) { } } -// Proof 19: completely missing origin remote on a daybook cortex fails closed -// across reads and writes — Put returns writer_unavailable; Get/Log/Lint return -// cortex_unavailable; zero fallback to c.Path. func TestProof19MissingOriginFailsClosedAcrossReadsAndWrites(t *testing.T) { testConfigEnv(t) base := t.TempDir() diff --git a/internal/kernel/mcp_test.go b/internal/kernel/mcp_test.go index 7652307..ab89ba0 100644 --- a/internal/kernel/mcp_test.go +++ b/internal/kernel/mcp_test.go @@ -13,8 +13,6 @@ import ( "github.com/modelcontextprotocol/go-sdk/mcp" ) -// binPath is the built exocortex binary used for the MCP stdio -// round-trip; TestMain builds it once for the package. var binPath string func TestMain(m *testing.M) { @@ -35,8 +33,6 @@ func TestMain(m *testing.M) { os.Exit(code) } -// Proof 7: MCP face round-trip — get → put(expectedRevision) → get -// bumps the revision and preserves the payload apart from the stamp. func TestProof7MCPRoundTrip(t *testing.T) { f := newFixture(t) ctx := context.Background() @@ -84,6 +80,11 @@ func TestProof7MCPRoundTrip(t *testing.T) { t.Fatalf("get mismatch: isErr=%v body=%v", isErr, got) } + missing, isErr := call("exocortex_get", map[string]any{"path": "notes/missing.md", "cortex": "hosta"}) + if !isErr || missing["error"] != "not_found" { + t.Fatalf("missing get: isErr=%v body=%v", isErr, missing) + } + updated := mkNote("note", "mcp roundtrip v2") body, isErr = call("exocortex_put", map[string]any{ "path": "notes/mcp.md", "content": updated, @@ -105,7 +106,6 @@ func TestProof7MCPRoundTrip(t *testing.T) { t.Fatalf("provenance stamp missing:\n%s", disk) } - // Stale expectedRevision surfaces the pinned conflict as an error result. body, isErr = call("exocortex_put", map[string]any{ "path": "notes/mcp.md", "content": updated, "expectedRevision": rev1, "cortex": "hosta", diff --git a/internal/kernel/read.go b/internal/kernel/read.go index 5e8ff42..2d175bb 100644 --- a/internal/kernel/read.go +++ b/internal/kernel/read.go @@ -40,6 +40,9 @@ func (s readSnapshot) read(path string) ([]byte, error) { if s.sha != "" { raw, err := git(s.repo, "show", s.sha+":"+path) if err != nil { + if tree, treeErr := git(s.repo, "ls-tree", "-z", s.sha, "--", path); treeErr == nil && tree == "" { + return nil, fs.ErrNotExist + } return nil, err } return []byte(raw), nil @@ -363,13 +366,13 @@ func conflictForRequest(source *Conflict, operation, path string) *Conflict { } func snapshotReadConflict(c *Cortex, operation, path string, err error) *Conflict { - if c.VCS == "daybook" { - return snapshotUnavailable(operation, path, err) - } if errors.Is(err, fs.ErrNotExist) { return conflict("not_found", operation, path, "check the path; search the cortex to locate the note", nil) } + if c.VCS == "daybook" { + return snapshotUnavailable(operation, path, err) + } return conflict("read_failed", operation, path, "fix filesystem access and retry", map[string]any{"detail": err.Error()}) } diff --git a/internal/kernel/read_refresh_test.go b/internal/kernel/read_refresh_test.go index b8dbe1e..b6958de 100644 --- a/internal/kernel/read_refresh_test.go +++ b/internal/kernel/read_refresh_test.go @@ -2,6 +2,8 @@ package kernel import ( "context" + "errors" + "io/fs" "os" "os/exec" "path/filepath" @@ -106,6 +108,17 @@ func TestReadFailsWithoutUpstream(t *testing.T) { } } +func TestGetClassifiesMissingCommittedPath(t *testing.T) { + f := newFixture(t) + got, conf := Get(f.cs, "hosta", "notes/missing.md") + if got != nil || conf == nil || conf.Code != "not_found" || conf.Path != "notes/missing.md" { + t.Fatalf("got=%+v conflict=%#v", got, conf) + } + if _, err := (readSnapshot{repo: mustEffectiveRoot(&f.cs[0]), sha: "missing-commit"}).read("README.md"); errors.Is(err, fs.ErrNotExist) { + t.Fatalf("invalid snapshot classified as missing path: %v", err) + } +} + func TestGetManyDoesNotMixRevisionsAfterOriginAdvance(t *testing.T) { f := newFixture(t) notes := []struct {