syncro: capture Phase-1 live-test findings (email-delivery trap + response shapes)
Test campaign on Howard Test sandbox (customer 36118743) surfaced traps the skill must not
re-learn:
- invoice/estimate email endpoints return 200 "Email sent" even with NO recipient
(customer.email null) -> never report delivery from the 200; verify customer.email first.
- customer.email is UNIQUE tenant-wide; PUT /customers failure returns
{success:false,message:[...]} not {customer:{...}} -> check .success.
- POST/PUT /contacts return a FLAT object (.id), not {contact:{...}}; contact recipient
flags (primary/receives_invoices) are ignored via API.
- POST /contracts write succeeds but body doesn't echo the object; /contracts?customer_id
doesn't filter server-side -> GET-verify + client-filter.
New living log .claude/standards/syncro/test-findings.md; critical items folded into
syncro.md Hard Rules + Verified Response Shapes. Correction logged to errorlog.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -92,6 +92,21 @@ $PYTHON -c "import datetime; d = datetime.date(YYYY, M, D); print(d.strftime('%A
|
|||||||
|
|
||||||
If any check fails, complete the missing step before reporting done. This rule fires on initial estimate creation AND on every subsequent "add X to the estimate" request. Incident: 2026-05-22, UPS added to estimate #7189 without a ticket note — caught by Winter.
|
If any check fails, complete the missing step before reporting done. This rule fires on initial estimate creation AND on every subsequent "add X to the estimate" request. Incident: 2026-05-22, UPS added to estimate #7189 without a ticket note — caught by Winter.
|
||||||
|
|
||||||
|
**Emailing an invoice/estimate returns 200 "Email sent" even when it reached NOBODY — never
|
||||||
|
report delivery from the 200 alone.** `POST /invoices/{id}/email` and `/estimates/{id}/email`
|
||||||
|
take no recipient param; Syncro picks it from the record. If `customer.email` is null (and no
|
||||||
|
recipient contact is flagged), the send is a silent no-op that still returns HTTP 200. Before
|
||||||
|
emailing, confirm the customer has a valid `email`. Note `customer.email` is **UNIQUE
|
||||||
|
tenant-wide** — reusing one returns `{"success":false,"message":["Email has already been
|
||||||
|
taken"]}`; a `PUT /customers` that fails returns that error shape (not `{"customer":{...}}`),
|
||||||
|
so check `.success` on customer writes instead of assuming a silent no-op.
|
||||||
|
|
||||||
|
**Living test-findings log:** `.claude/standards/syncro/test-findings.md` records verified
|
||||||
|
behaviors/gotchas discovered while exercising the skill on the Howard Test sandbox
|
||||||
|
(customer 36118743). Consult + append to it as new API quirks surface — it is how the skill
|
||||||
|
avoids re-learning the same trap. Critical entries are folded into these Hard Rules and the
|
||||||
|
Verified Response Shapes table.
|
||||||
|
|
||||||
## Implementation
|
## Implementation
|
||||||
|
|
||||||
When invoked, use the Syncro REST API via `curl`. All requests include `?api_key=<key>` as query parameter (NOT in header — Syncro uses query param auth).
|
When invoked, use the Syncro REST API via `curl`. All requests include `?api_key=<key>` as query parameter (NOT in header — Syncro uses query param auth).
|
||||||
@@ -568,6 +583,13 @@ Every endpoint's response shape, verified against the live API. Parse exactly as
|
|||||||
| Add schedule line | POST `/schedules/{id}/line_items` | `{"schedule_line_item": {...}}` | `.schedule_line_item.id` |
|
| Add schedule line | POST `/schedules/{id}/line_items` | `{"schedule_line_item": {...}}` | `.schedule_line_item.id` |
|
||||||
| Update schedule line | PUT `/schedules/{id}/line_items/{li_id}` | `{"schedule_line_item": {...}}` | `.schedule_line_item.quantity`, `.schedule_line_item.price_retail` |
|
| Update schedule line | PUT `/schedules/{id}/line_items/{li_id}` | `{"schedule_line_item": {...}}` | `.schedule_line_item.quantity`, `.schedule_line_item.price_retail` |
|
||||||
| Delete schedule line | DELETE `/schedules/{id}/line_items/{li_id}` | `{"success": true}` | — |
|
| Delete schedule line | DELETE `/schedules/{id}/line_items/{li_id}` | `{"success": true}` | — |
|
||||||
|
| Create customer | POST `/customers` | `{"customer": {...}}` | `.customer.id` |
|
||||||
|
| Update customer | PUT `/customers/{id}` | **success** `{"customer": {...}}` / **error** `{"success": false, "message": [...]}` | check `.success` — email is unique tenant-wide |
|
||||||
|
| Create contact | POST `/contacts` | **FLAT** `{"id": N, "email": ...}` (NOT wrapped) | `.id` — GET-verify; null-parse ≠ failure |
|
||||||
|
| Update contact | PUT `/contacts/{id}` | **FLAT** `{"id": N, ...}` | `.id` — `primary`/`receives_invoices` flags are ignored via API |
|
||||||
|
| Create contract | POST `/contracts` | write OK but body does NOT echo the object | GET `/contracts` + match; never retry on null |
|
||||||
|
| Email invoice | POST `/invoices/{id}/email` | `{"message": "Email sent."}` | **200 even with NO recipient** — verify `customer.email` first |
|
||||||
|
| Email estimate | POST `/estimates/{id}/email` | `{"message": "Email sent"}` | **200 even with NO recipient** — verify `customer.email` first |
|
||||||
|
|
||||||
**Invoice GET line_items field names differ from ticket line_items:** `item` = product name, `name` = description, `price` = unit rate. Do not use `price_retail` when reading invoice line items.
|
**Invoice GET line_items field names differ from ticket line_items:** `item` = product name, `name` = description, `price` = unit rate. Do not use `price_retail` when reading invoice line items.
|
||||||
|
|
||||||
|
|||||||
51
.claude/standards/syncro/test-findings.md
Normal file
51
.claude/standards/syncro/test-findings.md
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
# Syncro Skill — Live Test Findings (running log)
|
||||||
|
|
||||||
|
Verified behaviors/gotchas discovered while testing `/syncro` against the live tenant on the
|
||||||
|
**Howard Test** sandbox customer (`customer_id 36118743`) + RMM-TEST-MACHINE. Newest on top.
|
||||||
|
Each entry is a rule the skill must not re-learn. Critical ones are also folded into
|
||||||
|
`syncro.md` (operating manual) and `api-reference.md` (breadth reference).
|
||||||
|
|
||||||
|
## 2026-07-10 — Phase 1 (PSA CRUD)
|
||||||
|
|
||||||
|
**Emailing an invoice/estimate returns 200 "Email sent" even when it delivered to NOBODY.**
|
||||||
|
`POST /invoices/{id}/email` and `POST /estimates/{id}/email` take **no recipient param** —
|
||||||
|
Syncro picks the recipient from the record server-side. If `customer.email` is null (and no
|
||||||
|
contact is a flagged recipient), the API still returns `{"message":"Email sent"}` HTTP 200 but
|
||||||
|
nothing is sent. **NEVER report an email as delivered based on the 200 alone.** Ensure the
|
||||||
|
customer has a valid `email` (or a recipient contact) BEFORE emailing, and confirm real
|
||||||
|
delivery out-of-band when it matters.
|
||||||
|
|
||||||
|
**`customer.email` is UNIQUE across the tenant.** `PUT /customers/{id}` with an email already
|
||||||
|
used elsewhere returns `{"success":false,"message":["Email has already been taken"]}` and does
|
||||||
|
NOT set it. For a test customer that must reach a real person whose address is already in
|
||||||
|
Syncro, use plus-addressing (`user+tag@domain`) — unique in Syncro, still routes to the inbox
|
||||||
|
IF the mail tenant has plus-addressing enabled (M365: off by default in some tenants — verify).
|
||||||
|
|
||||||
|
**`PUT /customers/{id}` uses two different response shapes — always check for the error shape.**
|
||||||
|
Success → `{"customer":{...}}`. Validation failure → `{"success":false,"message":[...]}` with
|
||||||
|
the field unchanged. Parsing only `.customer.email` hides the failure (looks like a silent
|
||||||
|
no-op). Read the raw body / check `.success == false` on customer writes.
|
||||||
|
|
||||||
|
**Contacts return a FLAT object, not wrapped.** `POST /contacts` and `PUT /contacts/{id}`
|
||||||
|
return `{"id":N,"name":...,"email":...}` directly — NOT `{"contact":{...}}`. Parse `.id`, not
|
||||||
|
`.contact.id`. (The wrapped-parse returns null and looks like a failure when the write
|
||||||
|
succeeded — a blind retry then DUPLICATES the contact. GET-verify, never retry.)
|
||||||
|
|
||||||
|
**Contact recipient flags (`primary`, `receives_invoices`, `receives_reports`) are NOT settable
|
||||||
|
via the API** — `PUT /contacts/{id}` accepts them in the body but silently ignores them (stay
|
||||||
|
null). To designate an invoice-recipient contact, set it in the Syncro GUI.
|
||||||
|
|
||||||
|
**`POST /contracts` write succeeds but the response does not echo the created object** (fields
|
||||||
|
parse null). GET `/contracts` and match to confirm creation — never retry on the null.
|
||||||
|
|
||||||
|
**`GET /contracts?customer_id=N` does NOT filter server-side** — it returns ALL contracts.
|
||||||
|
Filter client-side by `.customer_id`.
|
||||||
|
|
||||||
|
**Reinforces the standing rule:** null/blank fields in a Syncro write response are NOT proof of
|
||||||
|
failure. Syncro has no idempotency, so a blind retry duplicates. Always GET the resource to
|
||||||
|
confirm true state before any follow-up action.
|
||||||
|
|
||||||
|
### Verified-working this phase (no gotcha, for coverage record)
|
||||||
|
POST/GET customer; POST contact; POST ticket (+ number); POST ticket comment (hidden);
|
||||||
|
POST add_line_item; POST invoice (from ticket); POST estimate + line_items + PUT recalc;
|
||||||
|
POST appointment; POST schedule (paused). All parse under the shapes documented in `syncro.md`.
|
||||||
@@ -19,6 +19,8 @@ Categories (the `[type]` tag): _(none)_ = skill/command execution failure ·
|
|||||||
|
|
||||||
<!-- Append entries below this line -->
|
<!-- Append entries below this line -->
|
||||||
|
|
||||||
|
2026-07-10 | Howard-Home | syncro/email | [correction] assumed HTTP 200 {message:Email sent} meant the invoice/estimate actually reached the customer; correct is Syncro returns 200 even when customer.email is null (no recipient) - must verify a valid, UNIQUE customer.email before claiming delivery [ctx: customer=36118743 endpoints=POST_/invoices/{id}/email,/estimates/{id}/email]
|
||||||
|
|
||||||
2026-07-10 | Howard-Home | syncro/assets | [correction] assumed asset writes (POST/PUT create-update), policy_folder_id move, patches, and installed_applications were not doable via the public API; correct is the endpoints exist per the live OpenAPI spec and the prior 'policy_folder_id silently ignored' was a token missing the 'Assets - Policy Change' scope, not an API limitation [ctx: ref=reference-syncro-rmm-api-gui-only endpoint=PUT_/customer_assets/{id}]
|
2026-07-10 | Howard-Home | syncro/assets | [correction] assumed asset writes (POST/PUT create-update), policy_folder_id move, patches, and installed_applications were not doable via the public API; correct is the endpoints exist per the live OpenAPI spec and the prior 'policy_folder_id silently ignored' was a token missing the 'Assets - Policy Change' scope, not an API limitation [ctx: ref=reference-syncro-rmm-api-gui-only endpoint=PUT_/customer_assets/{id}]
|
||||||
|
|
||||||
2026-07-10 | GURU-BEAST-ROG | guruscan | detached full-chain wrapper died mid-RKill on TPM-PC (marker never written, task->Ready, rkill.log completed fine as orphan) — RKill's process sweep almost certainly killed its own hidden SYSTEM powershell parent; relaunched with -SkipScanners RKill [ctx: host=TPM-PC client=mineralogical-record chain-design-issue]
|
2026-07-10 | GURU-BEAST-ROG | guruscan | detached full-chain wrapper died mid-RKill on TPM-PC (marker never written, task->Ready, rkill.log completed fine as orphan) — RKill's process sweep almost certainly killed its own hidden SYSTEM powershell parent; relaunched with -SkipScanners RKill [ctx: host=TPM-PC client=mineralogical-record chain-design-issue]
|
||||||
|
|||||||
Reference in New Issue
Block a user