fix(server): party_source gate must not block writes to an established party
partySourceGate ran on every PUT/move at party-depth-or-below and rejected with 409 whenever the party lacked a registry row — including edits of files already filed under working/<party>/…. The gate is an ONBOARDING guard (don't let a typo'd/unregistered party folder be introduced), not a write gate: once the party directory exists on disk the party is established, so editing within it must succeed. Allow when <project>/<peer>/<party>/ already exists; keep the 409 only for introducing a brand-new unregistered party. This was surfaced by the browse markdown editor 409ing on save for an existing file under a party folder whose ssr/ row was missing or differently-cased. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d5ce4e1230
commit
9320515214
2 changed files with 24 additions and 0 deletions
|
|
@ -922,6 +922,16 @@ func partySourceGate(fsRoot, abs string) (reject bool, msg, party string) {
|
||||||
if source == "" {
|
if source == "" {
|
||||||
return false, "", "" // peer does no party gating (e.g. ssr/)
|
return false, "", "" // peer does no party gating (e.g. ssr/)
|
||||||
}
|
}
|
||||||
|
// The gate only guards INTRODUCING a new party. Once the party
|
||||||
|
// directory exists on disk the party is established, so a PUT/move
|
||||||
|
// into its existing subtree (e.g. editing a file already filed under
|
||||||
|
// working/<party>/…) must not be blocked — the registration check is
|
||||||
|
// an onboarding guard, not a write gate. Without this, editing any
|
||||||
|
// pre-existing file under a party folder whose registry row is
|
||||||
|
// missing or differently-cased 409s on save.
|
||||||
|
if fi, err := os.Stat(filepath.Join(fsRoot, project, peer, p)); err == nil && fi.IsDir() {
|
||||||
|
return false, "", p
|
||||||
|
}
|
||||||
if zddc.PartyRegistered(filepath.Join(fsRoot, project), source, p) {
|
if zddc.PartyRegistered(filepath.Join(fsRoot, project), source, p) {
|
||||||
return false, "", p
|
return false, "", p
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -854,6 +854,20 @@ func TestFileAPI_PartySourceGate(t *testing.T) {
|
||||||
if _, err := os.Stat(filepath.Join(root, "Proj", "working", "Acme", "drafts")); err != nil {
|
if _, err := os.Stat(filepath.Join(root, "Proj", "working", "Acme", "drafts")); err != nil {
|
||||||
t.Errorf("workspace folder not created: %v", err)
|
t.Errorf("workspace folder not created: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Regression: a party folder that already exists on disk but whose
|
||||||
|
// registry row is missing (deleted, migrated, or differently-cased)
|
||||||
|
// must still accept writes. The gate guards INTRODUCING a party, not
|
||||||
|
// editing one that's established — without this, opening any
|
||||||
|
// pre-existing file under such a folder and saving it 409s. Create
|
||||||
|
// the folder out-of-band (no ssr/ row) and PUT into it.
|
||||||
|
if err := os.MkdirAll(filepath.Join(root, "Proj", "working", "Ghost"), 0o755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
rec = do(http.MethodPut, "/Proj/working/Ghost/note.md", "alice@example.com", []byte("# hi\n"), nil)
|
||||||
|
if rec.Code != http.StatusCreated && rec.Code != http.StatusOK {
|
||||||
|
t.Errorf("PUT into existing unregistered party folder: want 201/200, got %d: %s", rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// An in-place rename of a markdown file carries its .history/<stem>/ folder
|
// An in-place rename of a markdown file carries its .history/<stem>/ folder
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue