fix(api): validate the claim envelope on supersede, same as on create
hyperguild#111: writeHallNote validates half_life/applies/claim_provenance before a note is born; UpdateNote's supersede path didn't, so the fields that need provenance most (a whole-note regenerate is more likely to carry model-authored frontmatter than a fresh create) went unchecked. Fixed by extracting any frontmatter fence the caller leads Content with, validating it with the same claim.ValidateEnvelope, and merging its fields into the restamped frontmatter instead of leaving it as a second, inert `---`-fenced block in the body — that merge was a real gap independent of validation: a caller's supersede-time frontmatter was previously silently dropped, never taking effect at all. Reject leaves the note byte-identical (fail-closed, nothing written). Fence-less content (the common case today) is unchanged. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -52,6 +52,91 @@ func TestUpdateNoteSupersedesAndStamps(t *testing.T) {
|
||||
assert.Contains(t, s, "supersede_reason: facts changed")
|
||||
}
|
||||
|
||||
func TestUpdateNoteRejectsInvalidEnvelopeAndWritesNothing(t *testing.T) {
|
||||
// hyperguild#111: WriteNote validates the claim envelope before create;
|
||||
// UpdateNote didn't before supersede, so the same rule must hold on
|
||||
// change as on birth, fail-closed, nothing written on rejection.
|
||||
brainDir := t.TempDir()
|
||||
original := "---\nwing: jepa-fx\nhall: facts\ncreated_at: 2026-01-01T00:00:00Z\n---\n# Old\n\nold body\n"
|
||||
rel := seedNote(t, brainDir, "wiki/jepa-fx/facts/val-vol.md", original)
|
||||
|
||||
_, _, _, err := UpdateNote(brainDir, UpdateNoteOptions{
|
||||
Path: rel,
|
||||
Content: "---\nhalf_life: forever\n---\n# New\n\nnew body\n",
|
||||
})
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "claim envelope")
|
||||
|
||||
got, err := os.ReadFile(filepath.Join(brainDir, filepath.FromSlash(rel)))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, original, string(got), "rejected update must leave the note byte-identical")
|
||||
}
|
||||
|
||||
func TestUpdateNoteRejectsAppliesWithoutProvenance(t *testing.T) {
|
||||
brainDir := t.TempDir()
|
||||
original := "---\nwing: jepa-fx\nhall: facts\n---\nold\n"
|
||||
rel := seedNote(t, brainDir, "wiki/jepa-fx/facts/val-vol.md", original)
|
||||
|
||||
_, _, _, err := UpdateNote(brainDir, UpdateNoteOptions{
|
||||
Path: rel,
|
||||
Content: "---\napplies: universal\n---\nnew\n",
|
||||
})
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "claim envelope")
|
||||
|
||||
got, err := os.ReadFile(filepath.Join(brainDir, filepath.FromSlash(rel)))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, original, string(got))
|
||||
}
|
||||
|
||||
func TestUpdateNoteAcceptsValidEnvelopeAndMergesIntoFrontmatter(t *testing.T) {
|
||||
brainDir := t.TempDir()
|
||||
rel := seedNote(t, brainDir, "wiki/jepa-fx/facts/val-vol.md",
|
||||
"---\nwing: jepa-fx\nhall: facts\ncreated_at: 2026-01-01T00:00:00Z\n---\nold\n")
|
||||
|
||||
_, _, _, err := UpdateNote(brainDir, UpdateNoteOptions{
|
||||
Path: rel,
|
||||
Content: "---\nhalf_life: durable\napplies: universal\nclaim_provenance: asserted\n---\nnew body\n",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
got, err := os.ReadFile(filepath.Join(brainDir, filepath.FromSlash(rel)))
|
||||
require.NoError(t, err)
|
||||
s := string(got)
|
||||
assert.Contains(t, s, "half_life: durable")
|
||||
assert.Contains(t, s, "applies: universal")
|
||||
assert.Contains(t, s, "claim_provenance: asserted")
|
||||
assert.Contains(t, s, "wing: jepa-fx", "prior fields survive")
|
||||
assert.Contains(t, s, "new body")
|
||||
assert.Equal(t, 2, strings.Count(s, "---\n"),
|
||||
"exactly one open+close fence pair — the caller's own fence must not leak in as a second, inert block")
|
||||
}
|
||||
|
||||
func TestUpdateNoteWithoutEnvelopeFieldsIsUnchangedFromToday(t *testing.T) {
|
||||
// Regression pin: plain fence-less content (the common case) must behave
|
||||
// exactly as it did before the envelope-merge fix.
|
||||
brainDir := t.TempDir()
|
||||
rel := seedNote(t, brainDir, "wiki/jepa-fx/facts/val-vol.md",
|
||||
"---\nwing: jepa-fx\nhall: facts\ncreated_at: 2026-01-01T00:00:00Z\ncustom: keep-me\n---\n# Old\n\nold body\n")
|
||||
|
||||
relPath, hash, priorHash, err := UpdateNote(brainDir, UpdateNoteOptions{
|
||||
Path: rel,
|
||||
Content: "# New\n\nnew body\n",
|
||||
Reason: "facts changed",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, rel, relPath)
|
||||
assert.NotEmpty(t, hash)
|
||||
assert.NotEqual(t, hash, priorHash)
|
||||
|
||||
got, err := os.ReadFile(filepath.Join(brainDir, filepath.FromSlash(rel)))
|
||||
require.NoError(t, err)
|
||||
s := string(got)
|
||||
assert.Contains(t, s, "# New")
|
||||
assert.Contains(t, s, "custom: keep-me")
|
||||
assert.Contains(t, s, "supersedes: "+priorHash)
|
||||
}
|
||||
|
||||
func TestUpdateNoteResolvesByWingHallSlug(t *testing.T) {
|
||||
brainDir := t.TempDir()
|
||||
seedNote(t, brainDir, "wiki/jepa-fx/facts/val-vol.md",
|
||||
|
||||
Reference in New Issue
Block a user