memoryd: a Honcho client per workspace, and its workspace, peers and sessions made sure of
Co-Authored-By: ornith-1.5-35b-a3b <agent@wntrmute.dev>
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"path": "/v3/workspaces",
|
||||
"body": {
|
||||
"id": "boxmaker-plan-probe",
|
||||
"configuration": {
|
||||
"dream": {
|
||||
"enabled": false
|
||||
},
|
||||
"reasoning": {
|
||||
"enabled": false
|
||||
},
|
||||
"summary": {
|
||||
"enabled": false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"status": 201,
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": {
|
||||
"id": "boxmaker-plan-probe",
|
||||
"metadata": {},
|
||||
"configuration": {
|
||||
"reasoning": {
|
||||
"enabled": false
|
||||
},
|
||||
"summary": {
|
||||
"enabled": false
|
||||
},
|
||||
"dream": {
|
||||
"enabled": false
|
||||
}
|
||||
},
|
||||
"created_at": "2026-09-28T05:55:49.212118Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"path": "/v3/workspaces",
|
||||
"body": {
|
||||
"id": "boxmaker-plan-probe",
|
||||
"configuration": {
|
||||
"dream": {
|
||||
"enabled": false
|
||||
},
|
||||
"reasoning": {
|
||||
"enabled": false
|
||||
},
|
||||
"summary": {
|
||||
"enabled": false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": {
|
||||
"id": "boxmaker-plan-probe",
|
||||
"metadata": {},
|
||||
"configuration": {
|
||||
"reasoning": {
|
||||
"enabled": false
|
||||
},
|
||||
"summary": {
|
||||
"enabled": false
|
||||
},
|
||||
"dream": {
|
||||
"enabled": false
|
||||
}
|
||||
},
|
||||
"created_at": "2026-09-28T05:55:49.212118Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"path": "/v3/workspaces/boxmaker-plan-probe/peers",
|
||||
"body": {
|
||||
"id": "kyle"
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"status": 201,
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": {
|
||||
"id": "kyle",
|
||||
"workspace_id": "boxmaker-plan-probe",
|
||||
"created_at": "2026-09-28T05:55:56.303995Z",
|
||||
"metadata": {},
|
||||
"configuration": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"path": "/v3/workspaces/boxmaker-plan-probe/peers",
|
||||
"body": {
|
||||
"id": "boxmaker0",
|
||||
"configuration": {
|
||||
"observe_me": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"status": 201,
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": {
|
||||
"id": "boxmaker0",
|
||||
"workspace_id": "boxmaker-plan-probe",
|
||||
"created_at": "2026-09-28T05:55:56.326330Z",
|
||||
"metadata": {},
|
||||
"configuration": {
|
||||
"observe_me": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"path": "/v3/workspaces/boxmaker-plan-probe/peers",
|
||||
"body": {
|
||||
"id": "boxmaker0",
|
||||
"configuration": {
|
||||
"observe_me": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": {
|
||||
"id": "boxmaker0",
|
||||
"workspace_id": "boxmaker-plan-probe",
|
||||
"created_at": "2026-09-28T05:55:56.326330Z",
|
||||
"metadata": {},
|
||||
"configuration": {
|
||||
"observe_me": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"path": "/v3/workspaces/boxmaker-plan-probe/sessions",
|
||||
"body": {
|
||||
"id": "s-probe-1",
|
||||
"peers": {
|
||||
"kyle": {
|
||||
"observe_me": true,
|
||||
"observe_others": false
|
||||
},
|
||||
"boxmaker0": {
|
||||
"observe_me": false,
|
||||
"observe_others": false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"status": 201,
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": {
|
||||
"id": "s-probe-1",
|
||||
"is_active": true,
|
||||
"workspace_id": "boxmaker-plan-probe",
|
||||
"metadata": {},
|
||||
"configuration": {},
|
||||
"created_at": "2026-09-28T05:56:01.274224Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"path": "/v3/workspaces/boxmaker-plan-probe/sessions",
|
||||
"body": {
|
||||
"id": "s-probe-1",
|
||||
"peers": {
|
||||
"kyle": {
|
||||
"observe_me": true,
|
||||
"observe_others": false
|
||||
},
|
||||
"boxmaker0": {
|
||||
"observe_me": false,
|
||||
"observe_others": false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": {
|
||||
"id": "s-probe-1",
|
||||
"is_active": true,
|
||||
"workspace_id": "boxmaker-plan-probe",
|
||||
"metadata": {},
|
||||
"configuration": {},
|
||||
"created_at": "2026-09-28T05:56:01.274224Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"path": "/v3/workspaces/boxmaker-plan-probe/sessions/s-probe-1/messages",
|
||||
"body": {
|
||||
"messages": [
|
||||
{
|
||||
"content": "I prefer tea to coffee in the afternoon.",
|
||||
"peer_id": "kyle",
|
||||
"metadata": {
|
||||
"agent": "boxmaker0",
|
||||
"boxmaker_session": "s-probe-1",
|
||||
"turn": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": "Noted: tea in the afternoon.",
|
||||
"peer_id": "boxmaker0",
|
||||
"metadata": {
|
||||
"agent": "boxmaker0",
|
||||
"boxmaker_session": "s-probe-1",
|
||||
"turn": 1
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"status": 201,
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": [
|
||||
{
|
||||
"id": "7HqDWzXcKAdw9tnElCNFg",
|
||||
"content": "I prefer tea to coffee in the afternoon.",
|
||||
"peer_id": "kyle",
|
||||
"session_id": "s-probe-1",
|
||||
"metadata": {
|
||||
"agent": "boxmaker0",
|
||||
"boxmaker_session": "s-probe-1",
|
||||
"turn": 1
|
||||
},
|
||||
"created_at": "2026-09-28T05:56:08.742363Z",
|
||||
"workspace_id": "boxmaker-plan-probe",
|
||||
"token_count": 9
|
||||
},
|
||||
{
|
||||
"id": "mjsyhwuGsXTh1_zG_tWRx",
|
||||
"content": "Noted: tea in the afternoon.",
|
||||
"peer_id": "boxmaker0",
|
||||
"session_id": "s-probe-1",
|
||||
"metadata": {
|
||||
"agent": "boxmaker0",
|
||||
"boxmaker_session": "s-probe-1",
|
||||
"turn": 1
|
||||
},
|
||||
"created_at": "2026-09-28T05:56:08.742363Z",
|
||||
"workspace_id": "boxmaker-plan-probe",
|
||||
"token_count": 8
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"path": "/v3/workspaces/boxmaker-plan-probe/conclusions",
|
||||
"body": {
|
||||
"conclusions": [
|
||||
{
|
||||
"content": "The owner's builds run on straylight.",
|
||||
"observer_id": "boxmaker0",
|
||||
"observed_id": "boxmaker0",
|
||||
"session_id": "s-probe-1"
|
||||
},
|
||||
{
|
||||
"content": "Kyle prefers tea in the afternoon.",
|
||||
"observer_id": "kyle",
|
||||
"observed_id": "kyle",
|
||||
"session_id": "s-probe-1"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"status": 201,
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": [
|
||||
{
|
||||
"id": "YUBMqIqHhuJprV4o-inrg",
|
||||
"content": "The owner's builds run on straylight.",
|
||||
"observer_id": "boxmaker0",
|
||||
"observed_id": "boxmaker0",
|
||||
"session_id": "s-probe-1",
|
||||
"level": "explicit",
|
||||
"source_ids": null,
|
||||
"times_derived": 1,
|
||||
"created_at": "2026-09-28T05:56:22.130594Z"
|
||||
},
|
||||
{
|
||||
"id": "JuoCFtoo5lwFL7O4ySZ6f",
|
||||
"content": "Kyle prefers tea in the afternoon.",
|
||||
"observer_id": "kyle",
|
||||
"observed_id": "kyle",
|
||||
"session_id": "s-probe-1",
|
||||
"level": "explicit",
|
||||
"source_ids": null,
|
||||
"times_derived": 1,
|
||||
"created_at": "2026-09-28T05:56:22.130594Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"path": "/v3/workspaces/boxmaker-plan-probe/conclusions/query",
|
||||
"body": {
|
||||
"query": "builds",
|
||||
"filters": {
|
||||
"observed_id": "boxmaker0",
|
||||
"observer_id": "boxmaker0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": [
|
||||
{
|
||||
"id": "YUBMqIqHhuJprV4o-inrg",
|
||||
"content": "The owner's builds run on straylight.",
|
||||
"observer_id": "boxmaker0",
|
||||
"observed_id": "boxmaker0",
|
||||
"session_id": "s-probe-1",
|
||||
"level": "explicit",
|
||||
"source_ids": null,
|
||||
"times_derived": 1,
|
||||
"created_at": "2026-09-28T05:56:22.130594Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"path": "/v3/workspaces/boxmaker-plan-probe/conclusions/list",
|
||||
"body": {}
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": {
|
||||
"items": [
|
||||
{
|
||||
"id": "YUBMqIqHhuJprV4o-inrg",
|
||||
"content": "The owner's builds run on straylight.",
|
||||
"observer_id": "boxmaker0",
|
||||
"observed_id": "boxmaker0",
|
||||
"session_id": "s-probe-1",
|
||||
"level": "explicit",
|
||||
"source_ids": null,
|
||||
"times_derived": 1,
|
||||
"created_at": "2026-09-28T05:56:22.130594Z"
|
||||
},
|
||||
{
|
||||
"id": "JuoCFtoo5lwFL7O4ySZ6f",
|
||||
"content": "Kyle prefers tea in the afternoon.",
|
||||
"observer_id": "kyle",
|
||||
"observed_id": "kyle",
|
||||
"session_id": "s-probe-1",
|
||||
"level": "explicit",
|
||||
"source_ids": null,
|
||||
"times_derived": 1,
|
||||
"created_at": "2026-09-28T05:56:22.130594Z"
|
||||
}
|
||||
],
|
||||
"total": 2,
|
||||
"page": 1,
|
||||
"size": 50,
|
||||
"pages": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"path": "/v3/workspaces/boxmaker-plan-probe/conclusions/list",
|
||||
"body": {
|
||||
"filters": {
|
||||
"observed_id": "boxmaker0",
|
||||
"observer_id": "boxmaker0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": {
|
||||
"items": [
|
||||
{
|
||||
"id": "YUBMqIqHhuJprV4o-inrg",
|
||||
"content": "The owner's builds run on straylight.",
|
||||
"observer_id": "boxmaker0",
|
||||
"observed_id": "boxmaker0",
|
||||
"session_id": "s-probe-1",
|
||||
"level": "explicit",
|
||||
"source_ids": null,
|
||||
"times_derived": 1,
|
||||
"created_at": "2026-09-28T05:56:22.130594Z"
|
||||
}
|
||||
],
|
||||
"total": 1,
|
||||
"page": 1,
|
||||
"size": 50,
|
||||
"pages": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"request": {
|
||||
"method": "DELETE",
|
||||
"path": "/v3/workspaces/boxmaker-plan-probe/conclusions/YUBMqIqHhuJprV4o-inrg",
|
||||
"body": null
|
||||
},
|
||||
"response": {
|
||||
"status": 204,
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": ""
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"request": {
|
||||
"method": "DELETE",
|
||||
"path": "/v3/workspaces/boxmaker-plan-probe/conclusions/YUBMqIqHhuJprV4o-inrg",
|
||||
"body": null
|
||||
},
|
||||
"response": {
|
||||
"status": 404,
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": {
|
||||
"detail": "Document YUBMqIqHhuJprV4o-inrg not found or does not belong to workspace boxmaker-plan-probe"
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"path": "/v3/workspaces/boxmaker-plan-probe/chat",
|
||||
"body": {
|
||||
"query": "What does the owner drink in the afternoon?",
|
||||
"reasoning_level": "minimal"
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": {
|
||||
"content": "Based on what I found in memory, the owner in this workspace is **kyle**.\n\nThe relevant exchange (session s-probe-1, 2026-09-28):\n\n- **kyle**: \"I prefer tea to coffee in the afternoon.\"\n- **boxmaker0**: \"Noted: tea in the afternoon.\"\n\nSo the owner (kyle) drinks **tea** in the afternoon — specifically, tea rather than coffee.\n\nOne caveat: the second message from kyle in that conversation is a long run of repeated \"a\"s rather than a substantive statement, so there's no additional afternoon-drink detail beyond the tea preference.",
|
||||
"evidence": null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"path": "/v3/workspaces/boxmaker-plan-probe/sessions/s-missing/messages",
|
||||
"body": {
|
||||
"messages": [
|
||||
{
|
||||
"content": "probe",
|
||||
"peer_id": "kyle"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"status": 201,
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": [
|
||||
{
|
||||
"id": "HaXlV5eTh7GTEuID4JXir",
|
||||
"content": "probe",
|
||||
"peer_id": "kyle",
|
||||
"session_id": "s-missing",
|
||||
"metadata": {},
|
||||
"created_at": "2026-09-28T05:57:01.015107Z",
|
||||
"workspace_id": "boxmaker-plan-probe",
|
||||
"token_count": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"path": "/v3/workspaces/boxmaker-plan-probe/peers",
|
||||
"body": {
|
||||
"id": "bad peer"
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"status": 422,
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": {
|
||||
"detail": [
|
||||
{
|
||||
"type": "string_pattern_mismatch",
|
||||
"loc": [
|
||||
"body",
|
||||
"id"
|
||||
],
|
||||
"msg": "String should match pattern '^[a-zA-Z0-9_-]+$'",
|
||||
"input": "bad peer",
|
||||
"ctx": {
|
||||
"pattern": "^[a-zA-Z0-9_-]+$"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"path": "/v3/workspaces/boxmaker-plan-probe/sessions/s-never/messages/list",
|
||||
"body": {}
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": {
|
||||
"items": [],
|
||||
"total": 0,
|
||||
"page": 1,
|
||||
"size": 50,
|
||||
"pages": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"path": "/v3/workspaces/boxmaker-plan-probe/sessions/s-never/search",
|
||||
"body": {
|
||||
"query": "tea"
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": []
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"request": {
|
||||
"method": "DELETE",
|
||||
"path": "/v3/workspaces/boxmaker-plan-probe/sessions/s-never",
|
||||
"body": null
|
||||
},
|
||||
"response": {
|
||||
"status": 404,
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": {
|
||||
"detail": "Session s-never not found in workspace boxmaker-plan-probe"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"path": "/v3/workspaces/boxmaker-plan-probe/sessions/list",
|
||||
"body": {}
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": {
|
||||
"items": [
|
||||
{
|
||||
"id": "s-probe-1",
|
||||
"is_active": true,
|
||||
"workspace_id": "boxmaker-plan-probe",
|
||||
"metadata": {},
|
||||
"configuration": {},
|
||||
"created_at": "2026-09-28T05:56:01.274224Z"
|
||||
},
|
||||
{
|
||||
"id": "s-missing",
|
||||
"is_active": true,
|
||||
"workspace_id": "boxmaker-plan-probe",
|
||||
"metadata": {},
|
||||
"configuration": {},
|
||||
"created_at": "2026-09-28T05:57:00.994684Z"
|
||||
}
|
||||
],
|
||||
"total": 2,
|
||||
"page": 1,
|
||||
"size": 50,
|
||||
"pages": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"request": {
|
||||
"method": "DELETE",
|
||||
"path": "/v3/workspaces/boxmaker-plan-probe/sessions/s-probe-1",
|
||||
"body": null
|
||||
},
|
||||
"response": {
|
||||
"status": 202,
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": {
|
||||
"message": "Session deleted successfully"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"request": {
|
||||
"method": "DELETE",
|
||||
"path": "/v3/workspaces/boxmaker-plan-probe/sessions/s-missing",
|
||||
"body": null
|
||||
},
|
||||
"response": {
|
||||
"status": 202,
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": {
|
||||
"message": "Session deleted successfully"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"path": "/v3/workspaces/boxmaker-plan-probe/sessions/list",
|
||||
"body": {}
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": {
|
||||
"items": [],
|
||||
"total": 0,
|
||||
"page": 1,
|
||||
"size": 50,
|
||||
"pages": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"request": {
|
||||
"method": "DELETE",
|
||||
"path": "/v3/workspaces/boxmaker-plan-probe",
|
||||
"body": null
|
||||
},
|
||||
"response": {
|
||||
"status": 202,
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": {
|
||||
"message": "Workspace deletion accepted"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
# Honcho responses, recorded
|
||||
|
||||
Real requests and responses from the owner's Honcho (3.2.0, rift), recorded on 2026-09-27 by the
|
||||
design model for M5c-1, in a throwaway workspace, `boxmaker-plan-probe`, created for it and deleted
|
||||
afterwards. Each request is exactly what `roncho` 1.0.0's blocking client sends. Each file is
|
||||
`{"request": {"method", "path", "body"}, "response": {"status", "headers", "body"}}`. The fake
|
||||
Honcho server in `memoryd`'s tests answers from them. Do not edit.
|
||||
@@ -0,0 +1,439 @@
|
||||
//! `memoryd`'s Honcho clients (M5c spec, sections 3 and 6), against a fake Honcho that answers
|
||||
//! from the responses recorded from rift: the workspace, the peers and the session made before
|
||||
//! anything is written, each once per run, with the fleet's settings and the agent unobserved;
|
||||
//! the errors by kind and status, never a body. The keys are in `honcho_keys.rs`. Do not edit.
|
||||
|
||||
#[path = "support/honcho.rs"]
|
||||
mod honcho;
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::ffi::OsString;
|
||||
use std::time::Duration;
|
||||
|
||||
use honcho::{FakeHoncho, Mode, Request};
|
||||
use memoryd::honcho::client::{
|
||||
CHAT_READ, CONNECT, Clients, HonchoError, READ, Timeouts, classify, fleet_configuration,
|
||||
};
|
||||
use memoryd::honcho::config::HonchoConfig;
|
||||
use memoryd::honcho::ledger::Workspace;
|
||||
use memoryd::honcho::naming::HonchoSession;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
const FAST: Timeouts = Timeouts {
|
||||
connect: Duration::from_secs(5),
|
||||
read: Duration::from_millis(300),
|
||||
};
|
||||
|
||||
fn config(url: &str) -> HonchoConfig {
|
||||
HonchoConfig {
|
||||
url: url.to_string(),
|
||||
agent: "boxmaker0".to_string(),
|
||||
owner: "kyle".to_string(),
|
||||
trusted: "boxmaker0-trusted".to_string(),
|
||||
untrusted: "boxmaker0-untrusted".to_string(),
|
||||
fleet: "fleet".to_string(),
|
||||
read: Vec::new(),
|
||||
keys: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn no_env(_: &str) -> Option<OsString> {
|
||||
None
|
||||
}
|
||||
|
||||
fn clients(fake: &FakeHoncho) -> Clients {
|
||||
Clients::new(&config(&fake.url), &no_env, FAST).unwrap().0
|
||||
}
|
||||
|
||||
fn session(name: &str, speakers: &[&str]) -> HonchoSession {
|
||||
HonchoSession {
|
||||
name: name.to_string(),
|
||||
speakers: speakers.iter().map(|s| s.to_string()).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
/// A request as (method, path, body).
|
||||
fn seen(requests: &[Request]) -> Vec<(String, String, Value)> {
|
||||
requests
|
||||
.iter()
|
||||
.map(|r| {
|
||||
(
|
||||
r.method.clone(),
|
||||
r.path.clone(),
|
||||
r.body.clone().unwrap_or(Value::Null),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn post(path: &str, body: Value) -> (String, String, Value) {
|
||||
("POST".to_string(), path.to_string(), body)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_timeouts() {
|
||||
assert_eq!(CONNECT, Duration::from_secs(5));
|
||||
assert_eq!(READ, Duration::from_secs(30));
|
||||
assert_eq!(CHAT_READ, Duration::from_secs(180));
|
||||
assert_eq!(
|
||||
Timeouts::WRITES,
|
||||
Timeouts {
|
||||
connect: CONNECT,
|
||||
read: READ
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn each_client_has_its_workspace_and_timeouts() {
|
||||
let fake = FakeHoncho::start();
|
||||
let c = clients(&fake);
|
||||
for (workspace, name) in [
|
||||
(Workspace::Trusted, "boxmaker0-trusted"),
|
||||
(Workspace::Untrusted, "boxmaker0-untrusted"),
|
||||
(Workspace::Fleet, "fleet"),
|
||||
] {
|
||||
let client = c.client(workspace);
|
||||
assert_eq!(client.workspace_id(), name);
|
||||
// `roncho`'s `Debug` shows the timeouts, and whether there is a key, never the key.
|
||||
let shown = format!("{client:?}");
|
||||
assert!(shown.contains("connect_timeout: 5s"), "{shown}");
|
||||
assert!(shown.contains("read_timeout: 300ms"), "{shown}");
|
||||
assert!(shown.contains("api_key_present: false"), "{shown}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_fleets_configuration_is_the_recorded_one() {
|
||||
let path = concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/fixtures/honcho/01-workspace-create.json"
|
||||
);
|
||||
let recorded: Value = serde_json::from_str(&std::fs::read_to_string(path).unwrap()).unwrap();
|
||||
assert_eq!(
|
||||
Value::Object(fleet_configuration()),
|
||||
recorded["request"]["body"]["configuration"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_session_is_made_after_its_workspace_and_peers() {
|
||||
let fake = FakeHoncho::start();
|
||||
let mut c = clients(&fake);
|
||||
c.ensure_session(Workspace::Trusted, &session("chat-1", &["kyle"]))
|
||||
.unwrap();
|
||||
let w = "/v3/workspaces/boxmaker0-trusted";
|
||||
assert_eq!(
|
||||
seen(&fake.requests()),
|
||||
vec![
|
||||
post("/v3/workspaces", json!({ "id": "boxmaker0-trusted" })),
|
||||
post(
|
||||
&format!("{w}/peers"),
|
||||
json!({ "id": "boxmaker0", "configuration": { "observe_me": false } })
|
||||
),
|
||||
post(&format!("{w}/peers"), json!({ "id": "kyle" })),
|
||||
post(
|
||||
&format!("{w}/sessions"),
|
||||
json!({ "id": "chat-1", "peers": {
|
||||
"boxmaker0": { "observe_me": false, "observe_others": false },
|
||||
"kyle": { "observe_me": true, "observe_others": false },
|
||||
} })
|
||||
),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn what_was_made_is_not_asked_for_again_in_this_run() {
|
||||
let fake = FakeHoncho::start();
|
||||
let mut c = clients(&fake);
|
||||
c.ensure_session(Workspace::Trusted, &session("chat-1", &["kyle"]))
|
||||
.unwrap();
|
||||
fake.clear();
|
||||
c.ensure_session(Workspace::Trusted, &session("chat-1", &["kyle"]))
|
||||
.unwrap();
|
||||
assert!(fake.requests().is_empty(), "{:?}", fake.requests());
|
||||
// A new speaker: only its peer and the next session.
|
||||
c.ensure_session(Workspace::Trusted, &session("chat-1-2", &["kyle", "ana_b"]))
|
||||
.unwrap();
|
||||
let w = "/v3/workspaces/boxmaker0-trusted";
|
||||
assert_eq!(
|
||||
seen(&fake.requests()),
|
||||
vec![
|
||||
post(&format!("{w}/peers"), json!({ "id": "ana_b" })),
|
||||
post(
|
||||
&format!("{w}/sessions"),
|
||||
json!({ "id": "chat-1-2", "peers": {
|
||||
"boxmaker0": { "observe_me": false, "observe_others": false },
|
||||
"kyle": { "observe_me": true, "observe_others": false },
|
||||
"ana_b": { "observe_me": true, "observe_others": false },
|
||||
} })
|
||||
),
|
||||
]
|
||||
);
|
||||
// Another workspace is made sure of on its own.
|
||||
fake.clear();
|
||||
c.ensure_session(Workspace::Untrusted, &session("chat-1", &["kyle"]))
|
||||
.unwrap();
|
||||
assert_eq!(fake.requests().len(), 4);
|
||||
assert!(
|
||||
fake.requests()
|
||||
.iter()
|
||||
.skip(1)
|
||||
.all(|r| r.path.starts_with("/v3/workspaces/boxmaker0-untrusted/"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn after_a_restart_everything_is_made_sure_of_again() {
|
||||
let fake = FakeHoncho::start();
|
||||
clients(&fake)
|
||||
.ensure_session(Workspace::Trusted, &session("chat-1", &["kyle"]))
|
||||
.unwrap();
|
||||
fake.clear();
|
||||
// Honcho answers 200 now: it has them all. That is success too.
|
||||
clients(&fake)
|
||||
.ensure_session(Workspace::Trusted, &session("chat-1", &["kyle"]))
|
||||
.unwrap();
|
||||
assert_eq!(fake.requests().len(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_fleet_is_made_with_its_settings_and_the_agent_alone() {
|
||||
let fake = FakeHoncho::start();
|
||||
let mut c = clients(&fake);
|
||||
c.ensure_session(Workspace::Fleet, &session("chat-1", &[]))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
seen(&fake.requests()),
|
||||
vec![
|
||||
post(
|
||||
"/v3/workspaces",
|
||||
json!({ "id": "fleet", "configuration": {
|
||||
"reasoning": { "enabled": false },
|
||||
"summary": { "enabled": false },
|
||||
"dream": { "enabled": false },
|
||||
} })
|
||||
),
|
||||
post(
|
||||
"/v3/workspaces/fleet/peers",
|
||||
json!({ "id": "boxmaker0", "configuration": { "observe_me": false } })
|
||||
),
|
||||
post(
|
||||
"/v3/workspaces/fleet/sessions",
|
||||
json!({ "id": "chat-1", "peers": {
|
||||
"boxmaker0": { "observe_me": false, "observe_others": false },
|
||||
} })
|
||||
),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_409_on_create_means_it_is_there() {
|
||||
let fake = FakeHoncho::start();
|
||||
let mut c = clients(&fake);
|
||||
fake.set(Mode::Status(409));
|
||||
c.ensure_workspace(Workspace::Trusted).unwrap();
|
||||
c.ensure_peer(Workspace::Trusted, "kyle").unwrap();
|
||||
fake.clear();
|
||||
c.ensure_workspace(Workspace::Trusted).unwrap();
|
||||
assert!(fake.requests().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn errors_by_kind_and_status() {
|
||||
let fake = FakeHoncho::start();
|
||||
let mut c = clients(&fake);
|
||||
let cases = [
|
||||
(
|
||||
500,
|
||||
HonchoError::Unreachable {
|
||||
kind: "server",
|
||||
status: Some(500),
|
||||
},
|
||||
),
|
||||
(
|
||||
503,
|
||||
HonchoError::Unreachable {
|
||||
kind: "server",
|
||||
status: Some(503),
|
||||
},
|
||||
),
|
||||
(401, HonchoError::KeyRefused { status: 401 }),
|
||||
(403, HonchoError::KeyRefused { status: 403 }),
|
||||
(
|
||||
400,
|
||||
HonchoError::Refused {
|
||||
kind: "status",
|
||||
status: Some(400),
|
||||
},
|
||||
),
|
||||
(
|
||||
404,
|
||||
HonchoError::Refused {
|
||||
kind: "status",
|
||||
status: Some(404),
|
||||
},
|
||||
),
|
||||
(
|
||||
422,
|
||||
HonchoError::Refused {
|
||||
kind: "status",
|
||||
status: Some(422),
|
||||
},
|
||||
),
|
||||
];
|
||||
for (status, expected) in cases {
|
||||
fake.set(Mode::Status(status));
|
||||
assert_eq!(
|
||||
c.ensure_workspace(Workspace::Trusted),
|
||||
Err(expected.clone())
|
||||
);
|
||||
assert_eq!(c.ensure_peer(Workspace::Fleet, "kyle"), Err(expected));
|
||||
}
|
||||
// Nothing failed is remembered: the next call asks again, and succeeds.
|
||||
fake.set(Mode::Fixtures);
|
||||
fake.clear();
|
||||
c.ensure_workspace(Workspace::Trusted).unwrap();
|
||||
assert_eq!(fake.requests().len(), 1);
|
||||
fake.set(Mode::Hang);
|
||||
assert_eq!(
|
||||
c.ensure_peer(Workspace::Trusted, "ana_b"),
|
||||
Err(HonchoError::Unreachable {
|
||||
kind: "timeout",
|
||||
status: None
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nothing_listening_is_unreachable() {
|
||||
let port = {
|
||||
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
|
||||
listener.local_addr().unwrap().port()
|
||||
};
|
||||
let (mut c, _) =
|
||||
Clients::new(&config(&format!("http://127.0.0.1:{port}")), &no_env, FAST).unwrap();
|
||||
assert_eq!(
|
||||
c.ensure_workspace(Workspace::Trusted),
|
||||
Err(HonchoError::Unreachable {
|
||||
kind: "connect",
|
||||
status: None
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_error_never_holds_the_response() {
|
||||
let fake = FakeHoncho::start();
|
||||
let mut c = clients(&fake);
|
||||
fake.set(Mode::Status(422));
|
||||
let err = c
|
||||
.ensure_peer(Workspace::Trusted, "private-note-text")
|
||||
.unwrap_err();
|
||||
let body = fake.requests()[0].body.clone().unwrap();
|
||||
assert_eq!(body["id"], "private-note-text", "the fake did not echo it");
|
||||
for shown in [err.to_string(), format!("{err:?}")] {
|
||||
assert!(!shown.contains("private-note-text"), "{shown}");
|
||||
}
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"Honcho refused the request (status, HTTP 422)"
|
||||
);
|
||||
assert_eq!((err.kind(), err.status()), ("status", Some(422)));
|
||||
let lines = [
|
||||
HonchoError::Unreachable {
|
||||
kind: "timeout",
|
||||
status: None,
|
||||
}
|
||||
.to_string(),
|
||||
HonchoError::KeyRefused { status: 401 }.to_string(),
|
||||
];
|
||||
assert_eq!(lines[0], "Honcho is unreachable (timeout)");
|
||||
assert_eq!(lines[1], "Honcho refused the key (key-refused, HTTP 401)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_roncho_error_is_classified() {
|
||||
use roncho::blocking::Error;
|
||||
let io = || std::io::Error::other("x");
|
||||
let cases = [
|
||||
(Error::Connect(io()), "connect", None, true),
|
||||
(Error::Timeout, "timeout", None, true),
|
||||
(Error::Closed, "closed", None, true),
|
||||
(Error::Io(io()), "io", None, true),
|
||||
(
|
||||
Error::Status {
|
||||
status: 502,
|
||||
body: "b".into(),
|
||||
},
|
||||
"server",
|
||||
Some(502),
|
||||
true,
|
||||
),
|
||||
(
|
||||
Error::Status {
|
||||
status: 499,
|
||||
body: "b".into(),
|
||||
},
|
||||
"status",
|
||||
Some(499),
|
||||
false,
|
||||
),
|
||||
(
|
||||
Error::Redirect { status: 301 },
|
||||
"redirect",
|
||||
Some(301),
|
||||
false,
|
||||
),
|
||||
(
|
||||
Error::ActiveSessions { body: "b".into() },
|
||||
"active-sessions",
|
||||
Some(409),
|
||||
false,
|
||||
),
|
||||
(Error::TooLarge, "too-large", None, false),
|
||||
(Error::Decode("b".into()), "decode", None, false),
|
||||
(Error::Stream("b".into()), "stream", None, false),
|
||||
(Error::Config("b".into()), "config", None, false),
|
||||
];
|
||||
for (error, kind, status, unreachable) in cases {
|
||||
let got = classify(&error);
|
||||
assert_eq!((got.kind(), got.status()), (kind, status), "{error:?}");
|
||||
assert_eq!(
|
||||
matches!(got, HonchoError::Unreachable { .. }),
|
||||
unreachable,
|
||||
"{error:?}"
|
||||
);
|
||||
}
|
||||
let got = classify(&Error::Unauthorized { status: 403 });
|
||||
assert_eq!(got, HonchoError::KeyRefused { status: 403 });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_conclusion_deleted_or_gone_is_done() {
|
||||
let fake = FakeHoncho::start();
|
||||
let c = clients(&fake);
|
||||
c.delete_conclusion(Workspace::Trusted, "YUBMqIqHhuJprV4o-inrg")
|
||||
.unwrap();
|
||||
c.delete_conclusion(Workspace::Trusted, "YUBMqIqHhuJprV4o-inrg")
|
||||
.unwrap();
|
||||
let requests = fake.requests();
|
||||
assert_eq!(requests.len(), 2);
|
||||
assert!(requests.iter().all(|r| r.method == "DELETE"
|
||||
&& r.path == "/v3/workspaces/boxmaker0-trusted/conclusions/YUBMqIqHhuJprV4o-inrg"));
|
||||
fake.set(Mode::Status(500));
|
||||
assert_eq!(
|
||||
c.delete_conclusion(Workspace::Trusted, "x"),
|
||||
Err(HonchoError::Unreachable {
|
||||
kind: "server",
|
||||
status: Some(500)
|
||||
})
|
||||
);
|
||||
fake.set(Mode::Status(403));
|
||||
assert_eq!(
|
||||
c.delete_conclusion(Workspace::Trusted, "x"),
|
||||
Err(HonchoError::KeyRefused { status: 403 })
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
//! The keys of `memoryd`'s Honcho clients (M5c spec, section 10): each read with
|
||||
//! `bxcore::secrets`, sent as a bearer token to its own workspace only, and never shown by a
|
||||
//! client, an error or a warning; a key that cannot be read is an error naming it. Against a fake
|
||||
//! Honcho. Do not edit.
|
||||
|
||||
#[path = "support/honcho.rs"]
|
||||
mod honcho;
|
||||
#[path = "support/tmp.rs"]
|
||||
mod tmp;
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::ffi::OsString;
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::process::{Command, Stdio};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use bxcore::secrets::SecretSource;
|
||||
use honcho::{FakeHoncho, Mode};
|
||||
use memoryd::honcho::client::{Clients, Timeouts};
|
||||
use memoryd::honcho::config::{FLEET_KEY, HonchoConfig, TRUSTED_KEY};
|
||||
use memoryd::honcho::ledger::Workspace;
|
||||
use tmp::TempDir;
|
||||
|
||||
const FAST: Timeouts = Timeouts {
|
||||
connect: Duration::from_secs(5),
|
||||
read: Duration::from_millis(300),
|
||||
};
|
||||
|
||||
fn config(url: &str) -> HonchoConfig {
|
||||
HonchoConfig {
|
||||
url: url.to_string(),
|
||||
agent: "boxmaker0".to_string(),
|
||||
owner: "kyle".to_string(),
|
||||
trusted: "boxmaker0-trusted".to_string(),
|
||||
untrusted: "boxmaker0-untrusted".to_string(),
|
||||
fleet: "fleet".to_string(),
|
||||
read: Vec::new(),
|
||||
keys: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn no_env(_: &str) -> Option<OsString> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Key values built from pieces, so that no file holds one whole.
|
||||
fn env_key() -> String {
|
||||
["hk", "trusted", "5e1f0a"].join("-")
|
||||
}
|
||||
fn file_key() -> String {
|
||||
["hk", "fleet", "c27d94"].join("-")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn each_key_goes_to_its_own_workspace_and_is_never_shown() {
|
||||
let dir = TempDir::new("honcho-keys");
|
||||
let file = dir.write("fleet.key", &format!("{}\n", file_key()));
|
||||
std::fs::set_permissions(&file, std::fs::Permissions::from_mode(0o600)).unwrap();
|
||||
let fake = FakeHoncho::start();
|
||||
let mut config = config(&fake.url);
|
||||
config.keys.insert(
|
||||
TRUSTED_KEY.to_string(),
|
||||
SecretSource::Env("HONCHO_TRUSTED".to_string()),
|
||||
);
|
||||
config
|
||||
.keys
|
||||
.insert(FLEET_KEY.to_string(), SecretSource::File(file.clone()));
|
||||
let env = |name: &str| (name == "HONCHO_TRUSTED").then(|| OsString::from(env_key()));
|
||||
let (mut c, warnings) = Clients::new(&config, &env, FAST).unwrap();
|
||||
assert_eq!(warnings.len(), 1, "{warnings:?}");
|
||||
assert!(warnings[0].contains("honcho_fleet"), "{}", warnings[0]);
|
||||
c.ensure_workspace(Workspace::Trusted).unwrap();
|
||||
c.ensure_workspace(Workspace::Untrusted).unwrap();
|
||||
c.ensure_workspace(Workspace::Fleet).unwrap();
|
||||
let auth: Vec<Option<String>> = fake
|
||||
.requests()
|
||||
.iter()
|
||||
.map(|r| r.header("authorization").map(str::to_string))
|
||||
.collect();
|
||||
assert_eq!(
|
||||
auth,
|
||||
vec![
|
||||
Some(format!("Bearer {}", env_key())),
|
||||
None,
|
||||
Some(format!("Bearer {}", file_key())),
|
||||
]
|
||||
);
|
||||
// Nothing shows a key: the clients, their errors, the warnings.
|
||||
fake.set(Mode::Status(401));
|
||||
let err = c.ensure_peer(Workspace::Trusted, "kyle").unwrap_err();
|
||||
let shown = [
|
||||
format!("{c:?}"),
|
||||
format!("{err:?}"),
|
||||
err.to_string(),
|
||||
warnings.join("\n"),
|
||||
];
|
||||
for text in shown {
|
||||
assert!(!text.contains(&env_key()), "{text}");
|
||||
assert!(!text.contains(&file_key()), "{text}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_key_that_cannot_be_read_is_an_error_naming_it() {
|
||||
let fake = FakeHoncho::start();
|
||||
let mut config = config(&fake.url);
|
||||
config.keys.insert(
|
||||
TRUSTED_KEY.to_string(),
|
||||
SecretSource::Env("HONCHO_TRUSTED".to_string()),
|
||||
);
|
||||
let err = Clients::new(&config, &no_env, FAST).unwrap_err();
|
||||
assert!(err.contains("honcho_trusted"), "{err}");
|
||||
assert!(
|
||||
err.contains("see docs/runbook.md#secret-unavailable"),
|
||||
"{err}"
|
||||
);
|
||||
}
|
||||
|
||||
/// Set in the process `a_roncho_toml_in_home_is_not_read` starts, and only there.
|
||||
const CHILD: &str = "MEMORYD_TEST_RONCHO_HOME";
|
||||
|
||||
#[test]
|
||||
fn a_roncho_toml_in_home_is_not_read() {
|
||||
// `roncho` reads `$HOME/.config/roncho/roncho.toml` unless told not to, and takes its
|
||||
// `api_key`. HOME is the process's, so the check runs in a process of its own: this test
|
||||
// binary, running only the test below.
|
||||
let dir = TempDir::new("honcho-home");
|
||||
let key = ["hk", "from", "roncho", "toml"].join("-");
|
||||
dir.write(
|
||||
".config/roncho/roncho.toml",
|
||||
&format!("api_key = \"{key}\"\n"),
|
||||
);
|
||||
let mut child = Command::new(std::env::current_exe().unwrap())
|
||||
.args(["--exact", "in_a_home_with_a_roncho_toml", "--ignored"])
|
||||
.env("HOME", dir.path())
|
||||
.env_remove("XDG_CONFIG_HOME")
|
||||
.env(CHILD, "1")
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.unwrap();
|
||||
let deadline = Instant::now() + Duration::from_secs(20);
|
||||
let mut status = None;
|
||||
while Instant::now() < deadline {
|
||||
if let Ok(Some(done)) = child.try_wait() {
|
||||
status = Some(done);
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(20));
|
||||
}
|
||||
let _ = child.kill();
|
||||
let output = child.wait_with_output().unwrap();
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
assert!(status.is_some_and(|s| s.success()), "{stdout}");
|
||||
assert!(stdout.contains("1 passed"), "{stdout}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "run by a_roncho_toml_in_home_is_not_read, in a process of its own"]
|
||||
fn in_a_home_with_a_roncho_toml() {
|
||||
if std::env::var_os(CHILD).is_none() {
|
||||
return;
|
||||
}
|
||||
let fake = FakeHoncho::start();
|
||||
let (mut c, _) = Clients::new(&config(&fake.url), &no_env, FAST).unwrap();
|
||||
c.ensure_workspace(Workspace::Trusted).unwrap();
|
||||
let requests = fake.requests();
|
||||
assert_eq!(requests.len(), 1);
|
||||
assert_eq!(requests[0].header("authorization"), None);
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
//! Which Honcho session a turn goes to (M5c spec, section 3): the Boxmaker session's id, then
|
||||
//! `<id>-2`, `<id>-3` for each new speaker, holding every speaker so far; numbered from the
|
||||
//! ledger's decisions alone, so a restart numbers the same. Do not edit.
|
||||
|
||||
#[path = "support/tmp.rs"]
|
||||
mod tmp;
|
||||
|
||||
use bxcore::{SessionId, Timestamp};
|
||||
use memoryd::honcho::ledger::{Action, Item, Ledger, Record, Workspace};
|
||||
use memoryd::honcho::naming::{HonchoSession, honcho_session, session_name};
|
||||
use tmp::TempDir;
|
||||
|
||||
fn sid(s: &str) -> SessionId {
|
||||
SessionId::new(s).unwrap()
|
||||
}
|
||||
|
||||
fn turn(session: &str, turn: u64) -> Item {
|
||||
Item::Turn {
|
||||
session: sid(session),
|
||||
turn,
|
||||
}
|
||||
}
|
||||
|
||||
fn spoke(workspace: Workspace, speaker: &str) -> Action {
|
||||
Action::Turn {
|
||||
workspace,
|
||||
speaker: speaker.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn skip() -> Action {
|
||||
Action::Skip {
|
||||
why: "the turn has no single speaker".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn hs(name: &str, speakers: &[&str]) -> Option<HonchoSession> {
|
||||
Some(HonchoSession {
|
||||
name: name.to_string(),
|
||||
speakers: speakers.iter().map(|s| s.to_string()).collect(),
|
||||
})
|
||||
}
|
||||
|
||||
/// A ledger with these decisions, in this order.
|
||||
fn ledger(dir: &TempDir, decisions: &[(Item, Action)]) -> Ledger {
|
||||
let path = dir.path().join("honcho.jsonl");
|
||||
let mut ledger = Ledger::open(&path).unwrap();
|
||||
for (item, action) in decisions {
|
||||
ledger
|
||||
.append(Record::Decided {
|
||||
time: Timestamp::parse("2026-09-28T10:00:00.000Z").unwrap(),
|
||||
item: item.clone(),
|
||||
action: action.clone(),
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
ledger
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn session_names() {
|
||||
assert_eq!(session_name(&sid("chat-1"), 0), "chat-1");
|
||||
assert_eq!(session_name(&sid("chat-1"), 1), "chat-1");
|
||||
assert_eq!(session_name(&sid("chat-1"), 2), "chat-1-2");
|
||||
assert_eq!(session_name(&sid("chat-1"), 12), "chat-1-12");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn each_new_speaker_starts_the_next_session() {
|
||||
let dir = TempDir::new("naming-order");
|
||||
let decisions = [
|
||||
(turn("chat-1", 1), spoke(Workspace::Trusted, "kyle")),
|
||||
(turn("chat-2", 1), spoke(Workspace::Trusted, "zed")),
|
||||
(turn("chat-1", 2), spoke(Workspace::Trusted, "kyle")),
|
||||
(turn("chat-1", 3), spoke(Workspace::Untrusted, "ana_b")),
|
||||
(turn("chat-1", 4), skip()),
|
||||
(turn("chat-1", 5), spoke(Workspace::Trusted, "kyle")),
|
||||
(turn("chat-1", 6), spoke(Workspace::Untrusted, "bob")),
|
||||
(turn("chat-1", 7), spoke(Workspace::Trusted, "ana_b")),
|
||||
];
|
||||
let l = ledger(&dir, &decisions);
|
||||
let at = |t: u64| honcho_session(l.decisions(), &sid("chat-1"), t);
|
||||
assert_eq!(at(1), hs("chat-1", &["kyle"]));
|
||||
assert_eq!(at(2), hs("chat-1", &["kyle"]));
|
||||
// A new speaker in any workspace counts.
|
||||
assert_eq!(at(3), hs("chat-1-2", &["kyle", "ana_b"]));
|
||||
assert_eq!(at(4), None, "a skipped turn goes nowhere");
|
||||
assert_eq!(at(5), hs("chat-1-2", &["kyle", "ana_b"]));
|
||||
assert_eq!(at(6), hs("chat-1-3", &["kyle", "ana_b", "bob"]));
|
||||
assert_eq!(at(7), hs("chat-1-3", &["kyle", "ana_b", "bob"]));
|
||||
assert_eq!(at(8), None, "an undecided turn");
|
||||
// Another session's speakers do not count.
|
||||
assert_eq!(
|
||||
honcho_session(l.decisions(), &sid("chat-2"), 1),
|
||||
hs("chat-2", &["zed"])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_order_is_the_order_of_decision() {
|
||||
let dir = TempDir::new("naming-decided");
|
||||
// Turn 2 was decided first (turn 1 waited for brokerd): its speaker is the first.
|
||||
let decisions = [
|
||||
(turn("chat-1", 2), spoke(Workspace::Trusted, "ana_b")),
|
||||
(turn("chat-1", 1), spoke(Workspace::Trusted, "kyle")),
|
||||
];
|
||||
let l = ledger(&dir, &decisions);
|
||||
assert_eq!(
|
||||
honcho_session(l.decisions(), &sid("chat-1"), 2),
|
||||
hs("chat-1", &["ana_b"])
|
||||
);
|
||||
assert_eq!(
|
||||
honcho_session(l.decisions(), &sid("chat-1"), 1),
|
||||
hs("chat-1-2", &["ana_b", "kyle"])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_restart_numbers_the_same() {
|
||||
let dir = TempDir::new("naming-restart");
|
||||
let decisions = [
|
||||
(turn("chat-1", 1), spoke(Workspace::Trusted, "kyle")),
|
||||
(turn("chat-1", 2), skip()),
|
||||
(turn("chat-1", 3), spoke(Workspace::Trusted, "ana_b")),
|
||||
];
|
||||
let before: Vec<Option<HonchoSession>> = {
|
||||
let l = ledger(&dir, &decisions);
|
||||
(1..=3)
|
||||
.map(|t| honcho_session(l.decisions(), &sid("chat-1"), t))
|
||||
.collect()
|
||||
};
|
||||
let l = Ledger::open(&dir.path().join("honcho.jsonl")).unwrap();
|
||||
let after: Vec<Option<HonchoSession>> = (1..=3)
|
||||
.map(|t| honcho_session(l.decisions(), &sid("chat-1"), t))
|
||||
.collect();
|
||||
assert_eq!(before, after);
|
||||
assert_eq!(after[2], hs("chat-1-2", &["kyle", "ana_b"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decisions_are_every_decided_item_in_order() {
|
||||
let dir = TempDir::new("naming-decisions");
|
||||
let decisions = [
|
||||
(turn("chat-1", 2), skip()),
|
||||
(turn("chat-1", 1), spoke(Workspace::Trusted, "kyle")),
|
||||
];
|
||||
let l = ledger(&dir, &decisions);
|
||||
let got: Vec<(Item, Action)> = l.decisions().map(|(i, a)| (i.clone(), a.clone())).collect();
|
||||
assert_eq!(got, decisions.to_vec());
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
//! A fake Honcho for tests, on 127.0.0.1 and a port of its own: it answers each request from the
|
||||
//! responses recorded from rift (`tests/fixtures/honcho/`), by method and path, and records every
|
||||
//! request, body included. It can be set to answer every request with a status instead, or to
|
||||
//! answer nothing. Stopped, and its held connections released, when dropped; every wait has a
|
||||
//! deadline. Do not edit.
|
||||
|
||||
#![allow(dead_code)] // each test file uses a different part of this module
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::io::{Read, Write};
|
||||
use std::net::{TcpListener, TcpStream};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread::JoinHandle;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
/// One request as the fake read it.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Request {
|
||||
pub method: String,
|
||||
pub path: String,
|
||||
/// Header names in lower case.
|
||||
pub headers: Vec<(String, String)>,
|
||||
pub body: Option<Value>,
|
||||
}
|
||||
|
||||
impl Request {
|
||||
pub fn header(&self, name: &str) -> Option<&str> {
|
||||
self.headers
|
||||
.iter()
|
||||
.find(|(k, _)| k == name)
|
||||
.map(|(_, v)| v.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
/// How the fake answers.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Mode {
|
||||
/// From the fixtures.
|
||||
Fixtures,
|
||||
/// Every request with this status. A 422 echoes the request's body, as Honcho does.
|
||||
Status(u16),
|
||||
/// Read the request, then answer nothing until the fake is dropped (at most 30 s).
|
||||
Hang,
|
||||
}
|
||||
|
||||
struct Shared {
|
||||
mode: Mutex<Mode>,
|
||||
requests: Mutex<Vec<Request>>,
|
||||
/// What was made, by method and path and id: a second create answers 200, a second delete
|
||||
/// 404.
|
||||
made: Mutex<HashSet<String>>,
|
||||
stop: AtomicBool,
|
||||
}
|
||||
|
||||
pub struct FakeHoncho {
|
||||
pub url: String,
|
||||
port: u16,
|
||||
shared: Arc<Shared>,
|
||||
accept: Option<JoinHandle<()>>,
|
||||
}
|
||||
|
||||
/// A recorded fixture: its response's status and body.
|
||||
fn fixture(name: &str) -> (u16, String) {
|
||||
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("tests/fixtures/honcho")
|
||||
.join(name);
|
||||
let text = std::fs::read_to_string(&path).unwrap();
|
||||
let value: Value = serde_json::from_str(&text).unwrap();
|
||||
let status = u16::try_from(value["response"]["status"].as_u64().unwrap()).unwrap();
|
||||
let body = match &value["response"]["body"] {
|
||||
Value::String(s) if s.is_empty() => String::new(),
|
||||
other => other.to_string(),
|
||||
};
|
||||
(status, body)
|
||||
}
|
||||
|
||||
impl FakeHoncho {
|
||||
pub fn start() -> FakeHoncho {
|
||||
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
|
||||
let port = listener.local_addr().unwrap().port();
|
||||
let shared = Arc::new(Shared {
|
||||
mode: Mutex::new(Mode::Fixtures),
|
||||
requests: Mutex::new(Vec::new()),
|
||||
made: Mutex::new(HashSet::new()),
|
||||
stop: AtomicBool::new(false),
|
||||
});
|
||||
let for_accept = Arc::clone(&shared);
|
||||
let accept = std::thread::spawn(move || {
|
||||
for stream in listener.incoming() {
|
||||
if for_accept.stop.load(Ordering::SeqCst) {
|
||||
return;
|
||||
}
|
||||
let Ok(stream) = stream else { return };
|
||||
let shared = Arc::clone(&for_accept);
|
||||
std::thread::spawn(move || serve(stream, &shared));
|
||||
}
|
||||
});
|
||||
FakeHoncho {
|
||||
url: format!("http://127.0.0.1:{port}"),
|
||||
port,
|
||||
shared,
|
||||
accept: Some(accept),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set(&self, mode: Mode) {
|
||||
*self.shared.mode.lock().unwrap() = mode;
|
||||
}
|
||||
|
||||
/// Every request read so far, in order.
|
||||
pub fn requests(&self) -> Vec<Request> {
|
||||
self.shared.requests.lock().unwrap().clone()
|
||||
}
|
||||
|
||||
/// Forget the requests read so far (not what was made).
|
||||
pub fn clear(&self) {
|
||||
self.shared.requests.lock().unwrap().clear();
|
||||
}
|
||||
|
||||
/// Waits up to 10 s until at least `n` requests were read; true if they were.
|
||||
pub fn wait_for(&self, n: usize) -> bool {
|
||||
let deadline = Instant::now() + Duration::from_secs(10);
|
||||
while Instant::now() < deadline {
|
||||
if self.requests().len() >= n {
|
||||
return true;
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for FakeHoncho {
|
||||
fn drop(&mut self) {
|
||||
self.shared.stop.store(true, Ordering::SeqCst);
|
||||
let _ = TcpStream::connect(("127.0.0.1", self.port));
|
||||
if let Some(accept) = self.accept.take() {
|
||||
let _ = accept.join();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads one request: the head up to its blank line, then `Content-Length` bytes of body.
|
||||
fn read_request(stream: &mut TcpStream) -> Option<Request> {
|
||||
let mut buf = Vec::new();
|
||||
let mut byte = [0u8; 1];
|
||||
while !buf.ends_with(b"\r\n\r\n") {
|
||||
if stream.read(&mut byte).ok()? == 0 {
|
||||
return None;
|
||||
}
|
||||
buf.push(byte[0]);
|
||||
}
|
||||
let head = String::from_utf8(buf).ok()?;
|
||||
let mut lines = head.split("\r\n");
|
||||
let mut first = lines.next()?.split(' ');
|
||||
let (method, path) = (first.next()?.to_string(), first.next()?.to_string());
|
||||
let headers: Vec<(String, String)> = lines
|
||||
.filter_map(|l| l.split_once(": "))
|
||||
.map(|(k, v)| (k.to_ascii_lowercase(), v.to_string()))
|
||||
.collect();
|
||||
let length: usize = headers
|
||||
.iter()
|
||||
.find(|(k, _)| k == "content-length")
|
||||
.map_or(Some(0), |(_, v)| v.parse().ok())?;
|
||||
let mut body = vec![0u8; length];
|
||||
stream.read_exact(&mut body).ok()?;
|
||||
let body = if length == 0 {
|
||||
None
|
||||
} else {
|
||||
serde_json::from_slice(&body).ok()
|
||||
};
|
||||
Some(Request {
|
||||
method,
|
||||
path,
|
||||
headers,
|
||||
body,
|
||||
})
|
||||
}
|
||||
|
||||
/// The fixture that answers `request`, by its method and the shape of its path; the second of a
|
||||
/// pair when the same thing was made (or deleted) before.
|
||||
fn answer(request: &Request, made: &Mutex<HashSet<String>>) -> (u16, String) {
|
||||
let parts: Vec<&str> = request.path.trim_start_matches('/').split('/').collect();
|
||||
let id = request
|
||||
.body
|
||||
.as_ref()
|
||||
.and_then(|b| b["id"].as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string();
|
||||
let again = !made
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(format!("{} {} {id}", request.method, request.path));
|
||||
let configured = request
|
||||
.body
|
||||
.as_ref()
|
||||
.is_some_and(|b| b.get("configuration").is_some());
|
||||
match (request.method.as_str(), parts.as_slice()) {
|
||||
("POST", ["v3", "workspaces"]) if again => fixture("02-workspace-create-again.json"),
|
||||
("POST", ["v3", "workspaces"]) => fixture("01-workspace-create.json"),
|
||||
("POST", ["v3", "workspaces", _, "peers"]) if again => {
|
||||
fixture("05-peer-create-boxmaker0-again.json")
|
||||
}
|
||||
("POST", ["v3", "workspaces", _, "peers"]) if configured => {
|
||||
fixture("04-peer-create-boxmaker0.json")
|
||||
}
|
||||
("POST", ["v3", "workspaces", _, "peers"]) => fixture("03-peer-create-kyle.json"),
|
||||
("POST", ["v3", "workspaces", _, "sessions"]) if again => {
|
||||
fixture("07-session-create-again.json")
|
||||
}
|
||||
("POST", ["v3", "workspaces", _, "sessions"]) => fixture("06-session-create.json"),
|
||||
("POST", ["v3", "workspaces", _, "sessions", _, "messages"]) => {
|
||||
fixture("08-messages-add-batch.json")
|
||||
}
|
||||
("DELETE", ["v3", "workspaces", _, "conclusions", _]) if again => {
|
||||
fixture("16-conclusion-delete-again.json")
|
||||
}
|
||||
("DELETE", ["v3", "workspaces", _, "conclusions", _]) => {
|
||||
fixture("15-conclusion-delete.json")
|
||||
}
|
||||
_ => (404, r#"{"detail":"Not Found"}"#.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
fn serve(mut stream: TcpStream, shared: &Shared) {
|
||||
let _ = stream.set_read_timeout(Some(Duration::from_secs(10)));
|
||||
let Some(request) = read_request(&mut stream) else {
|
||||
return;
|
||||
};
|
||||
shared.requests.lock().unwrap().push(request.clone());
|
||||
let mode = *shared.mode.lock().unwrap();
|
||||
let (status, body) = match mode {
|
||||
Mode::Fixtures => answer(&request, &shared.made),
|
||||
Mode::Status(422) => {
|
||||
let input = request.body.clone().unwrap_or(Value::Null);
|
||||
(
|
||||
422,
|
||||
serde_json::json!({ "detail": [{ "input": input }] }).to_string(),
|
||||
)
|
||||
}
|
||||
Mode::Status(status) => (status, r#"{"detail":"refused"}"#.to_string()),
|
||||
Mode::Hang => {
|
||||
let deadline = Instant::now() + Duration::from_secs(30);
|
||||
while !shared.stop.load(Ordering::SeqCst) && Instant::now() < deadline {
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
return;
|
||||
}
|
||||
};
|
||||
let head = format!(
|
||||
"HTTP/1.1 {status} X\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
|
||||
body.len()
|
||||
);
|
||||
let _ = stream.write_all(head.as_bytes());
|
||||
let _ = stream.write_all(body.as_bytes());
|
||||
}
|
||||
Reference in New Issue
Block a user