okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
61c43ca
main
1"""Linked workspaces: one project in several places. 2 3A workspace linked with another reads that one's memory and work items as 4well as its own, while everything it writes is filed under itself. A member 5whose folder is gone keeps what it had until the user removes it, and 6unlinking deletes nothing. 7""" 8 9import json 10import re 11import shutil 12import unittest 13import urllib .error 14import urllib .parse 15 16from harness import Albedo ,Provider ,python ,text 17 18 19class LinkTests (unittest .TestCase ): 20def setUp (self ): 21self .cells = [] 22 23def script (request ): 24if request ["input" ][- 1 ].get ("role" )== "user" and self .cells : 25return python (self .cells .pop (0 )) 26return text ("ok" ) 27 28self .provider = Provider (script ) 29self .addCleanup (self .provider .close ) 30self .app = Albedo (self .provider ,protocol = "responses" ) 31self .app .__enter__ () 32self .addCleanup (self .app .__exit__ ,None ,None ,None ) 33 34def cell (self ,session ,code ): 35self .cells .append (code ) 36self .app .prompt (session ,"run it" ).close () 37self .app .idle (session ) 38results = [ 39json .loads (part ["value" ]) 40for entry in self .app .history (session )["items" ] 41if entry ["kind" ]== "tool_result" and entry ["tool" ]["name" ]== "python" 42for part in entry ["content" ] 43if part ["kind" ]== "json" and part ["field" ]== "result" 44 ] 45self .assertEqual (results [- 1 ]["status" ],"ok" ,results [- 1 ]) 46return results [- 1 ] 47 48def group (self ,workspace ): 49query = urllib .parse .urlencode ({"workspace" :str (workspace )}) 50with self .app .api (f"/extensions/links/groups? { query } " )as response : 51return json .load (response ) 52 53def merge (self ,workspace ,other ): 54group = self .group (workspace )["configuration_resource" ] 55other_group = self .group (other )["configuration_resource" ] 56with self .app .api ( 57group ["url" ], 58 {"other_workspace" :str (other ),"other_etag" :other_group ["etag" ]}, 59headers = {"If-Match" :group ["etag" ]}, 60 )as response : 61return json .load (response ) 62 63def unlink (self ,workspace ,member ): 64group = self .group (workspace )["configuration_resource" ] 65with self .app .api ( 66group ["url" ]+ "&" + urllib .parse .urlencode ({"member" :str (member )}), 67method = "DELETE" , 68headers = {"If-Match" :group ["etag" ]}, 69 )as response : 70return json .load (response ) 71 72def memory_file (self ,workspace ): 73slug = re .sub (r"[^A-Za-z0-9]" ,"-" ,str (workspace )) 74return self .app .home / "memories" / slug / "memory.md" 75 76def test_linked_workspaces_read_each_other_and_write_their_own (self ): 77laptop ,server = self .app .root / "laptop" ,self .app .root / "server" 78laptop .mkdir () 79server .mkdir () 80first = self .app .session (laptop ) 81self .cell ( 82first , 83"memory.append('the cache key is the prefix hash')\n" 84"await work.create('ship the picker')" , 85 ) 86 87self .app .session (server ) 88added = self .merge (server ,laptop ) 89self .assertEqual ( 90set (added ["resource" ]["value" ]["members" ]), {str (server ),str (laptop )} 91 ) 92group = self .group (server ) 93self .assertEqual ( 94set (group ["configuration_resource" ]["value" ]["members" ]), 95 {str (server ),str (laptop )}, 96 ) 97 98# A session opened after the link starts with the linked memory and 99# is told it is linked. 100later = self .app .session (server ) 101read = self .cell ( 102later , 103"memory.append('chernobog runs the gate')\n" 104"await work.create('profile the gate')\n" 105"print(memory.grep('cache key'))\n" 106"print(sorted((i.title, i.workspace) for i in await work.list()))" , 107 ) 108self .assertIn ( 109"[" + str (laptop )+ "] memory.md:1: the cache key" ,read ["output" ] 110 ) 111self .assertIn (f"('ship the picker', ' { laptop } ')" ,read ["output" ]) 112self .assertIn (f"('profile the gate', ' { server } ')" ,read ["output" ]) 113instructions = self .provider .requests [- 1 ]["request" ]["instructions" ] 114self .assertIn ("the cache key is the prefix hash" ,instructions ) 115self .assertIn ("This workspace is linked with " + str (laptop ),instructions ) 116# Writes stayed where they were made. 117self .assertEqual ( 118self .memory_file (server ).read_text (),"chernobog runs the gate\n" 119 ) 120self .assertEqual ( 121self .memory_file (laptop ).read_text (),"the cache key is the prefix hash\n" 122 ) 123 124# The laptop checkout goes away: it is shown as gone and still read. 125shutil .rmtree (laptop ) 126presence = {item ["workspace" ]:item for item in self .group (server )["presence" ]} 127self .assertEqual (presence [str (laptop )]["exists" ],False ) 128kept = self .cell (later ,"print(memory.grep('cache key'))" ) 129self .assertIn ("the cache key" ,kept ["output" ]) 130 131# Removing it stops the reads and deletes nothing. 132self .unlink (server ,laptop ) 133self .assertEqual ( 134self .group (server )["configuration_resource" ]["value" ]["members" ], 135 [str (server )], 136 ) 137alone = self .cell ( 138later , 139"print(memory.grep('cache key'))\n" 140"print([i.title for i in await work.list()])" , 141 ) 142self .assertNotIn ("the cache key" ,alone ["output" ]) 143self .assertNotIn ("ship the picker" ,alone ["output" ]) 144self .assertTrue (self .memory_file (laptop ).exists ()) 145 146def test_open_sessions_in_the_group_are_told_with_the_memory_they_now_read (self ): 147laptop ,server = self .app .root / "desk" ,self .app .root / "rack" 148laptop .mkdir () 149server .mkdir () 150on_laptop = self .app .session (laptop ) 151self .cell (on_laptop ,"memory.append('the cache key is the prefix hash')" ) 152here ,beside = self .app .session (server ),self .app .session (server ) 153self .cell (here ,"1" ) 154self .cell (beside ,"1" ) 155 156added = self .merge (server ,laptop ) 157self .assertEqual (added ["notification_count" ],3 ) 158self .assertEqual ( 159 {item ["session_id" ]for item in added ["notifications" ]}, 160 {on_laptop ,here ,beside }, 161 ) 162self .assertTrue ( 163all ( 164item ["notification" ]["state" ]== "queued" 165for item in added ["notifications" ] 166 ) 167 ) 168 169def next_request (session ): 170self .app .prompt (session ,"hi" ).close () 171self .app .idle (session ) 172request = self .provider .requests [- 1 ]["request" ] 173return json .dumps (request ["input" ]),request ["instructions" ] 174 175# Sessions in this workspace opened before the link: the laptop's 176# memory arrives in the note, not in a rebuilt prompt. 177for session in (here ,beside ): 178told ,instructions = next_request (session ) 179self .assertIn ("linked this workspace with " + str (laptop ),told ) 180self .assertIn ("the cache key is the prefix hash" ,told ) 181self .assertNotIn ("the cache key" ,instructions ) 182told ,_ = next_request (on_laptop ) 183self .assertIn ("linked this workspace with " + str (server ),told ) 184# A session created after the link starts with the linked memory. 185unopened = self .app .session (server ) 186told ,instructions = next_request (unopened ) 187self .assertNotIn ("linked this workspace" ,told ) 188self .assertIn ("the cache key is the prefix hash" ,instructions ) 189 190self .unlink (server ,laptop ) 191told ,_ = next_request (on_laptop ) 192self .assertIn ("unlinked this workspace from " + str (server ),told ) 193told ,_ = next_request (beside ) 194self .assertIn ("The user unlinked " + str (laptop ),told ) 195 196def test_a_link_is_refused_for_this_workspace_and_missing_folders (self ): 197workspace = self .app .root / "alone" 198workspace .mkdir () 199self .app .session (workspace ) 200for details ,status ,code in ( 201 (str (workspace ),409 ,"already_linked" ), 202 (str (self .app .root / "nowhere" ),400 ,"invalid_request" ), 203 ): 204with self .assertRaises (urllib .error .HTTPError )as refused : 205self .merge (workspace ,details ) 206self .assertEqual (refused .exception .code ,status ) 207self .assertEqual (json .load (refused .exception )["code" ],code ) 208self .assertEqual ( 209self .group (workspace )["configuration_resource" ]["value" ]["members" ], 210 [str (workspace )], 211 ) 212 213 214if __name__ == "__main__" : 215unittest .main ()