ver 1.0
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
#!/usr/bin/env python3
|
||||
"""mock_olrapi_hifi.py — H4: higher-fidelity mock of the Obsidian Local REST API.
|
||||
|
||||
The shipped mock's PATCH is a "naive append": it appends the body to EOF instead of
|
||||
inserting/replacing under the target heading, so the 0.9 surface (capture's
|
||||
_append_to_existing, scope set's replace, the daily Agent-Log insert) is only ever
|
||||
validated against behavior the real API does NOT exhibit. This subclass models the
|
||||
real PATCH semantics so the eval can catch heading-insert / replace regressions.
|
||||
|
||||
Over the base mock (eval/mock_olrapi.py) this overrides do_PATCH only:
|
||||
* heading append -> insert at the END of the target section's body (before the next
|
||||
sibling/parent heading), NOT at EOF.
|
||||
* heading prepend -> insert at the START of the section body (right after the heading).
|
||||
* heading replace -> replace the section body, keep the heading line.
|
||||
* heading missing -> 400 errorCode 40080 (the silent-write-loss trigger). [unchanged]
|
||||
* frontmatter replace -> parse the YAML block and set/replace the scalar; a replace on a
|
||||
MISSING field returns 400 40080 (matches the real API, per SKILL.md).
|
||||
|
||||
Everything else (GET/PUT/POST/DELETE, directory listing, document-map, flaky/phantom
|
||||
fault injection, debug reset) is inherited unchanged from the base handler.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
from http.server import ThreadingHTTPServer
|
||||
|
||||
import mock_olrapi
|
||||
from mock_olrapi import H as BaseH, STATE
|
||||
|
||||
|
||||
def _heading(line: str):
|
||||
m = re.match(r"^(#{1,6})\s+(.*)$", line)
|
||||
return (len(m.group(1)), m.group(2).strip()) if m else (0, None)
|
||||
|
||||
|
||||
def section_span(lines, target):
|
||||
"""(start, end, level) for the section under `target` (an "A::B" path; matched on its
|
||||
leaf). Body lines are lines[start+1:end]. None if the heading is absent."""
|
||||
leaf = target.split("::")[-1].strip()
|
||||
start = level = None
|
||||
for i, line in enumerate(lines):
|
||||
lv, txt = _heading(line)
|
||||
if lv and txt == leaf:
|
||||
start, level = i, lv
|
||||
break
|
||||
if start is None:
|
||||
return None
|
||||
end = len(lines)
|
||||
for j in range(start + 1, len(lines)):
|
||||
lv, _ = _heading(lines[j])
|
||||
if lv and lv <= level:
|
||||
end = j
|
||||
break
|
||||
return start, end, level
|
||||
|
||||
|
||||
def set_frontmatter(text: str, field: str, raw_value: str):
|
||||
"""Return (new_text, existed). raw_value is the JSON the client PATCHed."""
|
||||
try:
|
||||
value = json.loads(raw_value)
|
||||
except Exception:
|
||||
value = raw_value.strip().strip('"')
|
||||
rendered = value if isinstance(value, str) else json.dumps(value)
|
||||
new_line = f"{field}: {rendered}"
|
||||
if not text.startswith("---"):
|
||||
return f"---\n{new_line}\n---\n\n{text}", False
|
||||
end = text.find("\n---", 3)
|
||||
head = text[4:end] if end != -1 else text
|
||||
rest = text[end:] if end != -1 else "\n---\n"
|
||||
out, existed = [], False
|
||||
for line in head.splitlines():
|
||||
if re.match(rf"^{re.escape(field)}\s*:", line):
|
||||
out.append(new_line)
|
||||
existed = True
|
||||
else:
|
||||
out.append(line)
|
||||
if not existed:
|
||||
out.append(new_line)
|
||||
return "---\n" + "\n".join(out) + rest, existed
|
||||
|
||||
|
||||
class H(BaseH):
|
||||
def do_PATCH(self):
|
||||
vp = self._vpath()
|
||||
body = self._read_body()
|
||||
if vp is None:
|
||||
return self._json(400, {"message": "bad path"})
|
||||
ttype = self.headers.get("Target-Type", "")
|
||||
op = self.headers.get("Operation", "append")
|
||||
target = self.headers.get("Target", "")
|
||||
cur = STATE.get(vp, "")
|
||||
|
||||
if ttype == "heading":
|
||||
lines = cur.split("\n")
|
||||
span = section_span(lines, target)
|
||||
if span is None:
|
||||
return self._json(400, {"errorCode": 40080, "message": "invalid-target"})
|
||||
start, end, _ = span
|
||||
payload = body.strip("\n").split("\n") if body.strip("\n") else [""]
|
||||
if op == "replace":
|
||||
lines[start + 1:end] = payload
|
||||
elif op == "prepend":
|
||||
lines[start + 1:start + 1] = payload
|
||||
else: # append at the tail of the section body (before the next heading)
|
||||
ins = end
|
||||
while ins > start + 1 and lines[ins - 1].strip() == "":
|
||||
ins -= 1
|
||||
lines[ins:ins] = payload
|
||||
STATE[vp] = "\n".join(lines)
|
||||
return self._send(200, "")
|
||||
|
||||
if ttype == "frontmatter":
|
||||
new, existed = set_frontmatter(cur, target, body)
|
||||
if op == "replace" and not existed:
|
||||
return self._json(400, {"errorCode": 40080, "message": "invalid-target"})
|
||||
STATE[vp] = new
|
||||
return self._send(200, "")
|
||||
|
||||
STATE[vp] = cur + "\n" + body
|
||||
return self._send(200, "")
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--port", type=int, default=8800)
|
||||
a = ap.parse_args()
|
||||
srv = ThreadingHTTPServer(("127.0.0.1", a.port), H)
|
||||
print(f"mock-olrapi-hifi listening on http://127.0.0.1:{a.port}", flush=True)
|
||||
srv.serve_forever()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user