Why: the 8788 project-member runtime delivery finished (code merged via #526/#530) but its card and operation records lived only on an unpushed task branch, leaving the worktree impossible to recycle and the record invisible on main. Evidence: card status "已完成(运行目标)" on feature/project-member-runtime 835d807; branch diff against merge-base touches only .task (card + 29 operation records); credential scan finds only variable-based Authorization headers and sha256 digests. Impact: adds .task/archive/feature-project-member-runtime.md and .task/artifacts/project-member-runtime/**; no code, gitlink or policy change. Refs: worktree cleanup 2026-10-18 Co-authored-by: Cursor <cursoragent@cursor.com>
58 lines
5.0 KiB
Python
58 lines
5.0 KiB
Python
import hashlib,json,secrets,sqlite3
|
|
from pathlib import Path
|
|
from collections import Counter
|
|
import client,prepare
|
|
W=client.W;HOST=prepare.HOST
|
|
adopt=json.loads((W/'adoption-result.json').read_text());PID=adopt['canonicalProjectId']
|
|
state_path=W/'http-fixtures.json';state=json.loads(state_path.read_text()) if state_path.exists() else {'conversations':{},'artifacts':{}}
|
|
def state_save():client.save('http-fixtures.json',state)
|
|
def ok(owner,path,method='GET',data=None,raw=False,headers=None):
|
|
code,out=client.request(owner,path,method,data,raw,headers);assert code==200,f'{owner} {method} {path} status={code}';return out
|
|
def deny(owner,path,method='GET',data=None):
|
|
code,_=client.request(owner,path,method,data);assert code in (403,404),f'{owner} {path} not denied ({code})';return code
|
|
for owner in ['weichen','test']:
|
|
me=ok(owner,'/api/me');assert me['owner']==owner
|
|
projects=ok(owner,'/api/projects');p=next(x for x in projects if x['id']==HOST)
|
|
assert p['settings']['rdm.projectId']==PID and p['settings']['rdm.state']=='ready'
|
|
if owner not in state['conversations']:
|
|
conv=ok(owner,'/api/projects/'+HOST+'/conversations','POST',{'title':'成员协作复测-'+owner+'-20261013'})
|
|
assert conv['owner']==owner and conv['projectId']==HOST
|
|
state['conversations'][owner]=conv['id'];state_save()
|
|
own_id=state['conversations'][owner]
|
|
assert ok(owner,'/api/conversations/'+own_id)['conversation']['owner']==owner
|
|
if owner not in state['artifacts']:
|
|
marker=('PROJECT_MEMBER_RUNTIME_PRIVATE_'+owner+'_20261013\n').encode()
|
|
boundary='PMR'+secrets.token_hex(16)
|
|
raw=(f'--{boundary}\r\nContent-Disposition: form-data; name="file"; filename="成员协作复测-{owner}-私有.txt"\r\nContent-Type: text/plain\r\n\r\n').encode()+marker+(f'\r\n--{boundary}--\r\n').encode()
|
|
files=ok(owner,'/api/projects/'+HOST+'/uploads','POST',raw,headers={'Content-Type':'multipart/form-data; boundary='+boundary})
|
|
state['artifacts'][owner]={'id':files['artifactId'],'sha256':hashlib.sha256(marker).hexdigest()};state_save()
|
|
a=state['artifacts'][owner]
|
|
assert hashlib.sha256(ok(owner,'/api/artifacts/'+a['id']+'/content',raw=True)).hexdigest()==a['sha256']
|
|
with prepare.db(prepare.DATA/'agent-system.db') as c:
|
|
row=c.execute('select owner,project_id from artifacts where id=?',(a['id'],)).fetchone();assert tuple(row)==(owner,HOST)
|
|
private_status={}
|
|
for viewer,other in [('weichen','test'),('test','weichen')]:
|
|
private_status[viewer]={'other_conversation':deny(viewer,'/api/conversations/'+state['conversations'][other]),'other_original':deny(viewer,'/api/artifacts/'+state['artifacts'][other]['id']+'/content')}
|
|
assert state['conversations'][other] not in [x['id'] for x in ok(viewer,'/api/projects/'+HOST+'/conversations')]
|
|
assert state['artifacts'][other]['id'] not in [x['id'] for x in ok(viewer,'/api/projects/'+HOST+'/artifacts')]
|
|
foreign='prj_b32c62943fa3';foreign_pid='p-39dd859c3c99'
|
|
assert foreign not in [x['id'] for x in ok('test','/api/projects')]
|
|
foreign_status={'conversation_list':deny('test','/api/projects/'+foreign+'/conversations'),'conversation_create':deny('test','/api/projects/'+foreign+'/conversations','POST',{'title':'cross-project denial probe'}),'artifacts':deny('test','/api/projects/'+foreign+'/artifacts')}
|
|
code,denied=client.request('test','/api/commands/rdm.inspect.summary','POST',{'params':{'projectId':foreign_pid},'entrance':'workspace'})
|
|
assert code in (400,403,404) and denied.get('receipt',{}).get('state')=='rejected';foreign_status['domain_summary']=code
|
|
code,_=client.request('test','/api/projects','POST',{'name':'genui-e2e'});assert code==403,'member unexpectedly gained system project-create authority'
|
|
code,_=client.request('weichen','/api/projects','POST',{'name':'U7宿主项目'});assert code==409,'another owner project name was not globally reserved';duplicate_status=code
|
|
before=json.loads((W/'baseline-private.json').read_text());current=prepare.logical(prepare.DATA)
|
|
backup=Path(json.loads((W/'backup-location.json').read_text())['path'])
|
|
with prepare.db(backup/'dev/data/rdm.db') as c:
|
|
moved=Counter(prepare.digest(x) for x in c.execute('select * from rdm_members where project_id=?',(HOST,)))
|
|
preserved={}
|
|
for table,b in before['logical'].items():
|
|
expected=Counter(b['rows'])
|
|
if table=='rdm.db:rdm_members':expected-=moved
|
|
assert not expected-Counter(current[table]['rows']), 'old business rows changed: '+table
|
|
preserved[table]={'old_preserved':sum(expected.values()),'current':len(current[table]['rows'])}
|
|
for a in before['artifacts']:assert prepare.sha(Path(a['content_path']))==a['sha256']
|
|
result={'source':'SELF','hostProjectId':HOST,'canonicalProjectId':PID,'account_visibility_and_private_creation':'PASS','private_peer_status':private_status,'cross_project_status':foreign_status,'global_duplicate_name_status':duplicate_status,'preserved_business_rows':preserved,'original_artifact_bytes_preserved':len(before['artifacts']),'fixtures':state,'password_login':'NOT_RUN; user selected temporary API credentials'}
|
|
client.save('acceptance-http.json',result);print(json.dumps(result,ensure_ascii=False))
|