Skip to content
Merged
26 changes: 26 additions & 0 deletions python/tests/test_workspace_candidate_exports.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,32 @@ def export(self,candidate,format='reference-json',**options):
return self.client.post('/api/explore/revisions/'+candidate['revision_uuid']+'/exports',json={
'format':format,'expected_recipe_sha256':candidate['recipe']['full_recipe_sha256'],**options},headers=self.headers)

def test_local_search_exclusion_preserves_visible_query_and_exports_included_uuid(self):
base = {'field':'protocol','operator':'eq','value':'example'}
before = copy.deepcopy((self.case.curation.rows,self.case.protocol_bindings.rows))
visible = self.save(base)
self.assertEqual(visible['recipe']['epoch_count'],2)
excluded, included = self.service.ids
candidate = self.save({'all':[base,{'field':'epoch','operator':'not_in','value':[excluded]}]})
self.assertEqual(candidate['recipe']['epoch_count'],1)
response = self.export(candidate,'wheeler-sqlite')
self.assertEqual(response.status_code,201,response.get_json())
saved = self.store.get_dataset_revision(response.get_json()['dataset_uuid'])
self.assertEqual([row['uuid'] for row in saved['recipe']['epochs']],[included])
self.assertEqual(self.case.explorer_history.get(visible['revision_uuid'])['recipe']['predicate'],base)
self.assertEqual((self.case.curation.rows,self.case.protocol_bindings.rows),before)

def test_many_local_exclusions_in_bounded_literals_preserve_exact_membership(self):
excluded = [self.service.ids[0]] + [str(uuid.uuid4()) for _ in range(250)]
clauses = [{'field':'epoch','operator':'not_in','value':excluded[offset:offset+64]}
for offset in range(0,len(excluded),64)]
candidate = self.save({'all':[{'field':'protocol','operator':'eq','value':'example'},*clauses]})
self.assertEqual(candidate['recipe']['epoch_count'],1)
response = self.export(candidate,'wheeler-sqlite')
self.assertEqual(response.status_code,201,response.get_json())
saved = self.store.get_dataset_revision(response.get_json()['dataset_uuid'])
self.assertEqual([row['uuid'] for row in saved['recipe']['epochs']],self.service.ids[1:])

def test_focused_search_epoch_uuid_intersects_query_and_exports_one_row(self):
key = self.service.ids[1] # Both rows have the same block epoch number.
candidate = self.save({'all': [
Expand Down
18 changes: 18 additions & 0 deletions python/tests/test_workspace_protocol_binding.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,24 @@ def create_protocol(self,candidate,**overrides):
'expected_recipe_sha256':candidate['recipe']['content_sha256'],**overrides}
return self.client.post('/api/explore/revisions/'+candidate['revision_uuid']+'/create-protocol',json=body,headers=self.headers)

def test_search_local_exclusion_is_identical_for_updated_and_new_protocols(self):
base = {'field':'protocol','operator':'eq','value':'example'}
original = self.save_candidate(base)
candidate = self.save_candidate({'all':[base,{'field':'epoch','operator':'not_in','value':self.service.ids[:1]}]})
included = self.service.ids[1:]
self.assertEqual(candidate['recipe']['epoch_count'],1)
result = self.apply_candidate(candidate)
self.assertEqual(result.status_code,200,result.get_json())
updated = self.client.get(self.base+'/epochs').get_json()['epochs']
self.assertEqual([row['epoch_uuid'] for row in updated],included)
created = self.create_protocol(candidate)
self.assertEqual(created.status_code,201,created.get_json())
new_rows = self.client.get('/api/protocols/'+created.get_json()['protocol_uuid']+'/epochs').get_json()['epochs']
self.assertEqual([row['epoch_uuid'] for row in new_rows],included)
self.assertEqual(self.explorer_history.get(original['revision_uuid'])['recipe']['epoch_count'],2)
self.assertEqual(self.explorer_history.get(original['revision_uuid'])['recipe']['predicate'],base)
self.assertFalse(self.curation.rows)

def test_create_pinned_protocol_is_persistent_exact_and_retry_safe(self):
candidate=self.narrow(); old=copy.deepcopy(self.service.protocols[self.service.protocol_id])
result=self.create_protocol(candidate)
Expand Down
2 changes: 1 addition & 1 deletion workspace-app/src/App.jsx

Large diffs are not rendered by default.

13 changes: 13 additions & 0 deletions workspace-app/src/columnTreeNavigation.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,16 @@ export function columnSelectionNeedsAnchor(columns,selected,pending=false){
const leaf=columns.at(-1);
return pending||leaf?.kind!=='epochs'||!leaf.epochs?.some(epoch=>epoch.epoch_uuid===selected);
}

// Closing a column returns to its parent page without selecting scientific data.
export function columnBranchNavigation(page,item,expandedKey){
const opening=expandedKey!==item.key;
return {opening,path:opening?item.path:page.path,offset:opening?0:page.offset};
}

// Horizontal gestures belong to the whole column strip, including at its edges.
// Leave ordinary vertical gestures to the column under the pointer.
export function columnWheelDelta({deltaX=0,deltaY=0,shiftKey=false,deltaMode=0},width){
const horizontal=shiftKey?(deltaX||deltaY):Math.abs(deltaX)>Math.abs(deltaY)?deltaX:0;
return horizontal*(deltaMode===1?16:deltaMode===2?width:1);
}
17 changes: 16 additions & 1 deletion workspace-app/src/columnTreeNavigation.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {columnAncestorPages,canReuseColumn,columnSelectionNeedsAnchor} from './columnTreeNavigation.js';
import {columnAncestorPages,canReuseColumn,columnSelectionNeedsAnchor,columnBranchNavigation,columnWheelDelta} from './columnTreeNavigation.js';

test('selected epoch reconstructs ancestor pages beyond the first sixty branches',()=>{
const page={revision:'exact',path:['date-key','cell-key','block-key'],ancestors:[{parent_offset:120},{parent_offset:60},{parent_offset:180}]};
Expand All @@ -25,3 +25,18 @@ test('external selections already present need no request, while pending or othe
assert.equal(columnSelectionNeedsAnchor(columns,'shown',true),true);
assert.equal(columnSelectionNeedsAnchor(columns,null),false);
});

test('closing a column branch returns to its parent page without selecting a descendant',()=>{
const page={path:['date'],offset:60},cell={key:'cell3',path:['date','cell3']};
assert.deepEqual(columnBranchNavigation(page,cell,'cell3'),{opening:false,path:['date'],offset:60});
assert.deepEqual(columnBranchNavigation(page,cell,'cell5'),{opening:true,path:['date','cell3'],offset:0});
});


test('horizontal and Shift-wheel gestures cross columns while vertical gestures stay local',()=>{
assert.equal(columnWheelDelta({deltaX:90,deltaY:3},600),90);
assert.equal(columnWheelDelta({deltaX:-90,deltaY:3},600),-90);
assert.equal(columnWheelDelta({deltaX:3,deltaY:90},600),0);
assert.equal(columnWheelDelta({deltaY:4,shiftKey:true,deltaMode:1},600),64);
assert.equal(columnWheelDelta({deltaX:1,deltaMode:2},600),600);
});
2 changes: 1 addition & 1 deletion workspace-app/src/components/CandidateExportPanel.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export default function CandidateExportPanel({candidate,onExported,disabled=fals
}
return <section className="candidate-export" aria-label="Export saved search result">
<header><strong>Export this result</strong><span>{number(count)} epochs · one-off export</span></header>
<p>Export the saved query and tree directly. All matching epochs are included; protocol masks and tags are not merged. Any explicitly queried tags stay in the query evidence.</p>
<p>Export this selection’s included epochs and saved tree. Existing protocol masks and tags are not merged. Any explicitly queried tags stay in the query evidence.</p>
<form onSubmit={exportResult}>
<label className="candidate-export-name">Name <span>(optional)</span><input value={name} onChange={event=>changeName(event.target.value)} maxLength={120} placeholder={recipe?.name?`${recipe.name} · automatic date`:'Automatic name and date'} disabled={busy||disabled}/></label>
<fieldset disabled={busy||disabled}><legend>Handoff format</legend>
Expand Down
6 changes: 5 additions & 1 deletion workspace-app/src/components/ColumnTree.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading