feat: require estimate before create and start (Closes #849)
This commit is contained in:
parent
8edc83893f
commit
5c9420ed9c
|
|
@ -5,7 +5,40 @@ function createCreateAndStart({ todayWork, todaySync, refresh, warm, start, anno
|
|||
return todayWork.read().length < todayWork.limit;
|
||||
}
|
||||
|
||||
function complete(issue) {
|
||||
function capacity(value) {
|
||||
if (String(value ?? '').trim() === '') return { valid: false, reason: 'required' };
|
||||
const minutes = Number(value);
|
||||
if (!Number.isInteger(minutes) || minutes < 5 || minutes > 1440) {
|
||||
return { valid: false, reason: 'range' };
|
||||
}
|
||||
const plan = todayWork.planning();
|
||||
const capacityMinutes = plan.capacity_minutes;
|
||||
if (!Number.isInteger(capacityMinutes)) {
|
||||
return { valid: true, minutes, capacityMinutes: null, plannedMinutes: null,
|
||||
remainingMinutes: null, projectedMinutes: null, fits: true };
|
||||
}
|
||||
const plannedMinutes = todayWork.read().reduce(
|
||||
(total, identity) => total + (Number.isInteger(plan.estimates[identity]) ? plan.estimates[identity] : 0), 0
|
||||
);
|
||||
const remainingMinutes = capacityMinutes - plannedMinutes;
|
||||
const projectedMinutes = remainingMinutes - minutes;
|
||||
return { valid: true, minutes, capacityMinutes, plannedMinutes, remainingMinutes,
|
||||
projectedMinutes, fits: projectedMinutes >= 0 };
|
||||
}
|
||||
|
||||
function capacityMessage(result) {
|
||||
if (!result.valid) return result.reason === 'required'
|
||||
? 'Required only for Create & start.'
|
||||
: 'Use a whole number from 5 to 1440 minutes.';
|
||||
if (result.capacityMinutes === null) {
|
||||
return result.minutes + ' min · Set a Today budget to see remaining time.';
|
||||
}
|
||||
return result.fits
|
||||
? result.minutes + ' min · ' + result.projectedMinutes + ' min will remain in Today.'
|
||||
: result.minutes + ' min · ' + Math.abs(result.projectedMinutes) + ' min over Today capacity.';
|
||||
}
|
||||
|
||||
function complete(issue, estimateMinutes) {
|
||||
const identity = todayWork.identity(issue);
|
||||
if (!identity || completed.has(identity)) return 'exists';
|
||||
const added = todayWork.add(issue);
|
||||
|
|
@ -17,10 +50,25 @@ function createCreateAndStart({ todayWork, todaySync, refresh, warm, start, anno
|
|||
announce('Created, but Today could not be saved on this device.');
|
||||
return 'unavailable';
|
||||
}
|
||||
let estimatedPlan = null;
|
||||
if (Number.isInteger(estimateMinutes)) {
|
||||
estimatedPlan = todayWork.planning();
|
||||
estimatedPlan.estimates[identity] = estimateMinutes;
|
||||
if (!todayWork.replacePlanning(estimatedPlan)) {
|
||||
announce('Created and added to Today, but its estimate could not be saved on this device.');
|
||||
return 'estimate-unavailable';
|
||||
}
|
||||
}
|
||||
if (!todaySync.enqueue('add', identity)) {
|
||||
announce('Created and saved to Today on this device, but account sync is unavailable.');
|
||||
return 'sync-unavailable';
|
||||
}
|
||||
if (estimatedPlan) {
|
||||
if (!todaySync.enqueueConfiguration(estimatedPlan.capacity_minutes, estimatedPlan.estimates)) {
|
||||
announce('Created and estimated on this device, but Today plan sync is unavailable.');
|
||||
return 'sync-unavailable';
|
||||
}
|
||||
}
|
||||
completed.add(identity);
|
||||
refresh();
|
||||
todaySync.flush();
|
||||
|
|
@ -30,7 +78,7 @@ function createCreateAndStart({ todayWork, todaySync, refresh, warm, start, anno
|
|||
return 'started';
|
||||
}
|
||||
|
||||
return { available, complete };
|
||||
return { available, capacity, capacityMessage, complete };
|
||||
}
|
||||
|
||||
if (typeof module !== 'undefined' && module.exports) module.exports = createCreateAndStart;
|
||||
|
|
|
|||
|
|
@ -292,6 +292,9 @@ textarea { resize: vertical; min-height: 120px; }
|
|||
.create-issue-actions { display:grid; grid-template-columns:repeat(auto-fit,minmax(140px,1fr)); gap:8px; padding-bottom:env(safe-area-inset-bottom); }
|
||||
.create-issue-actions button { min-height:44px; max-width:100%; width:100%; }
|
||||
.create-issue-actions #create-issue-status { grid-column:1/-1; }
|
||||
.create-issue-estimate { display:grid; gap:6px; padding:10px; border:1px solid #31577f; border-radius:10px; background:#101d31; }
|
||||
.create-issue-estimate > span:first-of-type { display:flex; align-items:center; gap:8px; }
|
||||
.create-issue-estimate input { min-height:44px; width:min(9rem,60vw); }
|
||||
.draft-filing-session { position:sticky; top:0; z-index:2; display:grid; gap:6px; margin-bottom:12px; padding:10px; padding-bottom:calc(10px + env(safe-area-inset-bottom)); border:1px solid #31577f; border-radius:12px; background:rgba(11,21,38,.98); }
|
||||
.draft-filing-session[hidden] { display:none; }
|
||||
.draft-filing-session-actions { display:grid; grid-template-columns:1fr 1fr; gap:8px; }
|
||||
|
|
|
|||
|
|
@ -1594,6 +1594,12 @@
|
|||
},
|
||||
announce: message => { qs('#my-work-action-status').textContent = message; },
|
||||
});
|
||||
function renderCreateStartCapacity() {
|
||||
const result = createAndStart.capacity(qs('#create-issue-estimate').value);
|
||||
qs('#create-issue-estimate-status').textContent = createAndStart.capacityMessage(result);
|
||||
return result;
|
||||
}
|
||||
qs('#create-issue-estimate').addEventListener('input', renderCreateStartCapacity);
|
||||
|
||||
const queueToday = createQueueToday({
|
||||
todayWork,
|
||||
|
|
@ -3988,7 +3994,7 @@
|
|||
const created = lastMyWork.find(item => item.kind === 'issue' &&
|
||||
item.repository === completion.issue?.repository && item.number === completion.issue?.number);
|
||||
if (!created) return;
|
||||
const outcome = createAndStart.complete(created);
|
||||
const outcome = createAndStart.complete(created, completion.estimateMinutes);
|
||||
if (outcome === 'started' || outcome === 'exists') {
|
||||
issueOutbox.completeIntent(completion.id, activeFlushLogin);
|
||||
startedCompletions.add(created.repository + '#' + created.number);
|
||||
|
|
@ -5271,6 +5277,8 @@
|
|||
(event.submitter.id === 'create-follow-up-next' ? 'follow-up-and-next' : 'create-and-assign');
|
||||
const intent = pendingIssueFilingIntent;
|
||||
createAndStartRequested = intent === 'create-and-start';
|
||||
const startCapacity = createAndStartRequested
|
||||
? createAndStart.capacity(qs('#create-issue-estimate').value) : null;
|
||||
const captureDraft = currentIssueCaptureDraft();
|
||||
if (!captureDraft.repository || !captureDraft.title) {
|
||||
qs('#create-issue-status').textContent = 'Choose a repository and add a title.';
|
||||
|
|
@ -5288,6 +5296,21 @@
|
|||
qs('#create-and-start-issue').focus();
|
||||
return;
|
||||
}
|
||||
if (createAndStartRequested && !startCapacity.valid) {
|
||||
qs('#create-issue-status').textContent = startCapacity.reason === 'required'
|
||||
? 'Add a work estimate before creating and starting.'
|
||||
: 'Use a whole-number estimate from 5 to 1440 minutes.';
|
||||
qs('#create-issue-estimate').focus();
|
||||
renderCreateStartCapacity();
|
||||
return;
|
||||
}
|
||||
if (createAndStartRequested && !startCapacity.fits) {
|
||||
qs('#create-issue-status').textContent = 'This would put Today ' +
|
||||
Math.abs(startCapacity.projectedMinutes) + ' minutes over capacity. Adjust the estimate or Today plan.';
|
||||
qs('#create-issue-estimate').focus();
|
||||
renderCreateStartCapacity();
|
||||
return;
|
||||
}
|
||||
qs('#create-issue-status').textContent = 'Preparing complete filing review…';
|
||||
try {
|
||||
const evidence = await createIssueAttachmentController.serialize();
|
||||
|
|
@ -5298,7 +5321,10 @@
|
|||
milestoneTitle: captureDraft.milestoneId ? milestoneOption?.textContent?.trim() : '',
|
||||
...(Array.isArray(evidence) ? {attachments:evidence} : {attachment:evidence}),
|
||||
...(rUC ? { sourceCaptureId: rUC } : {}),
|
||||
...(createAndStartRequested ? { completionIntent: 'create-and-start' } : {}),
|
||||
...(createAndStartRequested ? {
|
||||
completionIntent: 'create-and-start', estimateMinutes: startCapacity.minutes,
|
||||
todayCapacity: startCapacity,
|
||||
} : {}),
|
||||
};
|
||||
filingReview.open({draft: durableDraft, intent}, event.submitter || qs('#submit-new-issue'));
|
||||
qs('#create-issue-status').textContent = 'Review the complete payload, then confirm filing.';
|
||||
|
|
|
|||
|
|
@ -838,6 +838,10 @@
|
|||
<div id="create-issue-duplicate-list" class="create-issue-duplicate-list"></div>
|
||||
<button id="create-issue-anyway" type="button" hidden>Create anyway</button>
|
||||
</section>
|
||||
<label class="create-issue-estimate" for="create-issue-estimate">Work estimate for Today
|
||||
<span><input id="create-issue-estimate" type="number" inputmode="numeric" min="5" max="1440" step="5" placeholder="45" aria-describedby="create-issue-estimate-status" /> min</span>
|
||||
<span id="create-issue-estimate-status" class="small" aria-live="polite">Required only for Create & start.</span>
|
||||
</label>
|
||||
<div class="small">The issue will be assigned to you.</div>
|
||||
<div class="create-issue-actions">
|
||||
<button id="submit-new-issue" type="submit">Create & assign to me</button>
|
||||
|
|
|
|||
|
|
@ -107,11 +107,20 @@
|
|||
const owner = draft.assignee
|
||||
? 'Owner: ' + (draft.assigneeName || draft.assignee) + ' (@' + draft.assignee + ')'
|
||||
: 'Assigned to you';
|
||||
const planning = [];
|
||||
if (Number.isInteger(draft.estimateMinutes)) {
|
||||
planning.push('Estimate: ' + draft.estimateMinutes + ' min');
|
||||
const projected = draft.todayCapacity?.projectedMinutes;
|
||||
if (Number.isInteger(projected)) planning.push(projected >= 0
|
||||
? 'Today after start: ' + projected + ' min free'
|
||||
: 'Today after start: ' + Math.abs(projected) + ' min over capacity');
|
||||
}
|
||||
options.metadata.textContent = [
|
||||
labels.length ? 'Labels: ' + labels.join(', ') : 'No labels',
|
||||
'Milestone: ' + milestone,
|
||||
'Due: ' + dueDate,
|
||||
owner,
|
||||
...planning,
|
||||
].join(' · ');
|
||||
if (options.blockerList) {
|
||||
options.blockerList.replaceChildren(...(draft.blockers || []).map(blocker => {
|
||||
|
|
|
|||
|
|
@ -91,7 +91,13 @@ function createIssueOutbox({ storage, fetchJson, coordinator, backgroundSync, ge
|
|||
}
|
||||
const sourceCaptureId = String(draft?.sourceCaptureId || '').trim().slice(0, 128);
|
||||
if (sourceCaptureId) item.sourceCaptureId = sourceCaptureId;
|
||||
if (draft?.completionIntent === 'create-and-start' && !assignee) item.completionIntent = 'create-and-start';
|
||||
if (draft?.completionIntent === 'create-and-start' && !assignee) {
|
||||
item.completionIntent = 'create-and-start';
|
||||
const estimateMinutes = Number(draft?.estimateMinutes);
|
||||
if (Number.isInteger(estimateMinutes) && estimateMinutes >= 5 && estimateMinutes <= 1440) {
|
||||
item.estimateMinutes = estimateMinutes;
|
||||
}
|
||||
}
|
||||
const attachment = captureAttachment(draft?.attachment);
|
||||
if (attachment) item.attachment = attachment;
|
||||
const attachments = captureAttachments(draft?.attachments);
|
||||
|
|
@ -216,6 +222,10 @@ function createIssueOutbox({ storage, fetchJson, coordinator, backgroundSync, ge
|
|||
|| JSON.stringify(item.labelIds || []) !== JSON.stringify(nextLabelIds)
|
||||
|| item.assignee !== nextAssignee || item.assigneeName !== nextAssigneeName
|
||||
|| item.milestoneId !== nextMilestoneId || item.dueDate !== nextDueDate
|
||||
|| item.estimateMinutes !== (() => {
|
||||
const value = Number(draft?.estimateMinutes);
|
||||
return Number.isInteger(value) && value >= 5 && value <= 1440 ? value : undefined;
|
||||
})()
|
||||
|| attachmentChanged || JSON.stringify(item.blockers || null) !== JSON.stringify(nextBlockers || null);
|
||||
updated = {
|
||||
...item,
|
||||
|
|
@ -227,8 +237,16 @@ function createIssueOutbox({ storage, fetchJson, coordinator, backgroundSync, ge
|
|||
operationId: changed ? String(operationId()).slice(0, 128) : item.operationId,
|
||||
status: 'queued',
|
||||
};
|
||||
if (draft?.completionIntent === 'create-and-start' && !nextAssignee) updated.completionIntent = 'create-and-start';
|
||||
else delete updated.completionIntent;
|
||||
if (draft?.completionIntent === 'create-and-start' && !nextAssignee) {
|
||||
updated.completionIntent = 'create-and-start';
|
||||
const estimateMinutes = Number(draft?.estimateMinutes);
|
||||
if (Number.isInteger(estimateMinutes) && estimateMinutes >= 5 && estimateMinutes <= 1440) {
|
||||
updated.estimateMinutes = estimateMinutes;
|
||||
} else delete updated.estimateMinutes;
|
||||
} else {
|
||||
delete updated.completionIntent;
|
||||
delete updated.estimateMinutes;
|
||||
}
|
||||
if (nextAssignee === undefined) {
|
||||
delete updated.assignee;
|
||||
delete updated.assigneeName;
|
||||
|
|
@ -460,6 +478,7 @@ function createIssueOutbox({ storage, fetchJson, coordinator, backgroundSync, ge
|
|||
intent: result.item.completionIntent,
|
||||
ownerLogin: result.item.ownerLogin,
|
||||
operationId: result.item.operationId,
|
||||
estimateMinutes: result.item.estimateMinutes,
|
||||
issue: result.issue,
|
||||
});
|
||||
}
|
||||
|
|
@ -499,6 +518,7 @@ function createIssueOutbox({ storage, fetchJson, coordinator, backgroundSync, ge
|
|||
intent: result.item.completionIntent,
|
||||
ownerLogin: result.item.ownerLogin,
|
||||
operationId: result.item.operationId,
|
||||
estimateMinutes: result.item.estimateMinutes,
|
||||
issue: result.issue,
|
||||
}] : [],
|
||||
remaining: read(), blocked: 0,
|
||||
|
|
@ -543,6 +563,7 @@ function createIssueOutbox({ storage, fetchJson, coordinator, backgroundSync, ge
|
|||
intent: item.completionIntent,
|
||||
ownerLogin: item.ownerLogin,
|
||||
operationId: item.operationId,
|
||||
estimateMinutes: item.estimateMinutes,
|
||||
issue: item.deliveredIssue,
|
||||
}));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ FEATURE_SOURCES = {
|
|||
"today-timer": (
|
||||
"static/today-completion.js", "static/work-detail-position.js", "static/commands.js", "static/saved-searches.js", "static/task-overlay-history.js", "static/search-preview.js", "static/search-defer.js", "static/mobile-search-viewport.js", "static/my-work.js", "static/protect-today.js", "static/mobile-task-dock.js", "static/mobile-queue-launcher.js", "static/update-triage-session.js", "static/update-review-handoff.js", "static/update-triage-launcher.js", "static/update-triage-gesture.js", "static/notification-undo.js", "static/today-timer.js", "static/today-recap.js",
|
||||
"static/today-rollover.js", "static/later-work.js", "static/drafts.js", "static/unfiled-captures.js",
|
||||
"static/assign-and-start.js", "static/queue-today.js",
|
||||
"static/assign-and-start.js", "static/queue-today.js", "static/create-and-start.js",
|
||||
"static/draft-filing-session.js", "static/draft-capacity-dialog.js", "static/work-selection.js",
|
||||
"static/today-work.js", "static/pick-work.js", "static/batch-find-work.js",
|
||||
"static/search-batch-plan.js", "static/issue-evidence-review.js", "static/issue-evidence-editor.js",
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ def run_node(script):
|
|||
).stdout)
|
||||
|
||||
|
||||
def test_confirmed_issue_is_planned_synced_warmed_and_started_once():
|
||||
def test_confirmed_issue_persists_and_syncs_estimate_before_starting_once():
|
||||
script = f"""
|
||||
const fs = require('fs');
|
||||
if (!fs.existsSync({json.dumps(str(CREATE_AND_START))})) {{
|
||||
|
|
@ -26,13 +26,22 @@ if (!fs.existsSync({json.dumps(str(CREATE_AND_START))})) {{
|
|||
const calls = [];
|
||||
const issue = {{kind:'issue', repository:'stackchain/dashboard', number:387}};
|
||||
const flow = createCreateAndStart({{
|
||||
todayWork: {{limit:5, read:()=>[], identity:item=>'issue:'+item.repository+':'+item.number+':', add:()=>{{calls.push('add');return 'added';}}}},
|
||||
todaySync: {{enqueue:(action,id)=>{{calls.push(action+':'+id);return true;}}, flush:()=>calls.push('flush')}},
|
||||
todayWork: {{
|
||||
limit:5, read:()=>[], identity:item=>'issue:'+item.repository+':'+item.number+':',
|
||||
add:()=>{{calls.push('add');return 'added';}},
|
||||
planning:()=>({{capacity_minutes:120,estimates:{{}}}}),
|
||||
replacePlanning:plan=>{{calls.push('plan:'+JSON.stringify(plan));return true;}},
|
||||
}},
|
||||
todaySync: {{
|
||||
enqueue:(action,id)=>{{calls.push(action+':'+id);return true;}},
|
||||
enqueueConfiguration:(capacity,estimates)=>{{calls.push('configure:'+capacity+':'+JSON.stringify(estimates));return true;}},
|
||||
flush:()=>calls.push('flush')
|
||||
}},
|
||||
refresh:()=>calls.push('refresh'), warm:()=>calls.push('warm'), start:item=>calls.push('start:'+item.number),
|
||||
announce:message=>calls.push('announce:'+message),
|
||||
}});
|
||||
const result = flow.complete(issue);
|
||||
flow.complete(issue);
|
||||
const result = flow.complete(issue, 45);
|
||||
flow.complete(issue, 45);
|
||||
process.stdout.write(JSON.stringify({{available:true,result,calls}}));
|
||||
}}
|
||||
"""
|
||||
|
|
@ -42,7 +51,9 @@ if (!fs.existsSync({json.dumps(str(CREATE_AND_START))})) {{
|
|||
"result": "started",
|
||||
"calls": [
|
||||
"add",
|
||||
'plan:{"capacity_minutes":120,"estimates":{"issue:stackchain/dashboard:387:":45}}',
|
||||
"add:issue:stackchain/dashboard:387:",
|
||||
'configure:120:{"issue:stackchain/dashboard:387:":45}',
|
||||
"refresh",
|
||||
"flush",
|
||||
"warm",
|
||||
|
|
@ -63,7 +74,12 @@ def test_mobile_capture_wires_distinct_create_and_start_intent_into_the_offline_
|
|||
assert "const createAndStart = createCreateAndStart({" in dashboard
|
||||
assert "event.submitter.id === 'create-and-start-issue'" in dashboard
|
||||
assert "review.intent === 'create-and-start'" in dashboard
|
||||
assert "createAndStart.complete(created)" in dashboard
|
||||
assert 'id="create-issue-estimate"' in html
|
||||
assert 'min="5" max="1440" step="5"' in html
|
||||
assert "createAndStart.capacity(qs('#create-issue-estimate').value)" in dashboard
|
||||
assert "estimateMinutes: startCapacity.minutes" in dashboard
|
||||
assert "createAndStart.complete(created, completion.estimateMinutes)" in dashboard
|
||||
assert ".create-issue-estimate" in css
|
||||
assert ".create-issue-actions" in css and "grid-template-columns" in css
|
||||
assert "BASE + 'static/create-and-start.js'" in worker
|
||||
|
||||
|
|
@ -126,3 +142,61 @@ process.stdout.write(JSON.stringify({{first,second,calls}}));
|
|||
"refresh", "flush", "warm", "start",
|
||||
"Created, added to Today, and ready to work.",
|
||||
]
|
||||
|
||||
|
||||
def test_create_and_start_reports_validation_and_remaining_today_capacity():
|
||||
script = f"""
|
||||
const createCreateAndStart=require({json.dumps(str(CREATE_AND_START))});
|
||||
const flow=createCreateAndStart({{
|
||||
todayWork:{{limit:5,read:()=>['first','second'],planning:()=>({{
|
||||
capacity_minutes:120,estimates:{{first:30,second:40}}
|
||||
}})}},
|
||||
}});
|
||||
process.stdout.write(JSON.stringify({{
|
||||
empty:flow.capacity(''),tooSmall:flow.capacity('4'),fits:flow.capacity('45'),over:flow.capacity('55')
|
||||
}}));
|
||||
"""
|
||||
assert run_node(script) == {
|
||||
"empty": {"valid": False, "reason": "required"},
|
||||
"tooSmall": {"valid": False, "reason": "range"},
|
||||
"fits": {"valid": True, "minutes": 45, "capacityMinutes": 120, "plannedMinutes": 70,
|
||||
"remainingMinutes": 50, "projectedMinutes": 5, "fits": True},
|
||||
"over": {"valid": True, "minutes": 55, "capacityMinutes": 120, "plannedMinutes": 70,
|
||||
"remainingMinutes": 50, "projectedMinutes": -5, "fits": False},
|
||||
}
|
||||
|
||||
|
||||
def test_create_and_start_does_not_start_when_estimate_cannot_be_saved():
|
||||
script = f"""
|
||||
const createCreateAndStart=require({json.dumps(str(CREATE_AND_START))});const calls=[];
|
||||
const flow=createCreateAndStart({{
|
||||
todayWork:{{limit:5,read:()=>[],identity:()=> 'issue:o/r:849:',add:()=> 'added',
|
||||
planning:()=>({{capacity_minutes:60,estimates:{{}}}}),replacePlanning:()=>false}},
|
||||
todaySync:{{enqueue:()=>true,enqueueConfiguration:()=>true,flush:()=>calls.push('flush')}},
|
||||
refresh:()=>calls.push('refresh'),warm:()=>calls.push('warm'),start:()=>calls.push('start'),
|
||||
announce:message=>calls.push(message),
|
||||
}});
|
||||
process.stdout.write(JSON.stringify({{result:flow.complete({{repository:'o/r',number:849}},30),calls}}));
|
||||
"""
|
||||
assert run_node(script) == {
|
||||
"result": "estimate-unavailable",
|
||||
"calls": ["Created and added to Today, but its estimate could not be saved on this device."],
|
||||
}
|
||||
|
||||
|
||||
def test_create_and_start_does_not_start_when_estimate_sync_cannot_be_queued():
|
||||
script = f"""
|
||||
const createCreateAndStart=require({json.dumps(str(CREATE_AND_START))});const calls=[];
|
||||
const flow=createCreateAndStart({{
|
||||
todayWork:{{limit:5,read:()=>[],identity:()=> 'issue:o/r:849:',add:()=> 'added',
|
||||
planning:()=>({{capacity_minutes:60,estimates:{{}}}}),replacePlanning:()=>true}},
|
||||
todaySync:{{enqueue:()=>true,enqueueConfiguration:()=>false,flush:()=>calls.push('flush')}},
|
||||
refresh:()=>calls.push('refresh'),warm:()=>calls.push('warm'),start:()=>calls.push('start'),
|
||||
announce:message=>calls.push(message),
|
||||
}});
|
||||
process.stdout.write(JSON.stringify({{result:flow.complete({{repository:'o/r',number:849}},30),calls}}));
|
||||
"""
|
||||
assert run_node(script) == {
|
||||
"result": "sync-unavailable",
|
||||
"calls": ["Created and estimated on this device, but Today plan sync is unavailable."],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -113,6 +113,23 @@ process.stdout.write(JSON.stringify({{issueType:issueType.textContent}}));
|
|||
assert run_node(script)["issueType"] == "Bug report"
|
||||
|
||||
|
||||
def test_create_and_start_review_shows_estimate_and_projected_capacity():
|
||||
script = f"""
|
||||
const createReview=require({json.dumps(str(MODULE))});
|
||||
function target(){{return{{hidden:true,disabled:false,textContent:'',addEventListener:()=>{{}},replaceChildren:()=>{{}},focus:()=>{{}}}};}}
|
||||
const metadata=target();
|
||||
const review=createReview({{sheet:target(),confirmButton:target(),backButton:target(),evidenceList:target(),
|
||||
repository:target(),intent:target(),title:target(),body:target(),metadata,
|
||||
document:{{createElement:()=>target(),addEventListener:()=>{{}}}},onConfirm:async()=>{{}}}});
|
||||
review.open({{draft:{{repository:'o/r',title:'Sized work',estimateMinutes:45,
|
||||
todayCapacity:{{remainingMinutes:60,projectedMinutes:15}}}},intent:'create-and-start'}},target());
|
||||
process.stdout.write(JSON.stringify({{metadata:metadata.textContent}}));
|
||||
"""
|
||||
metadata = run_node(script)["metadata"]
|
||||
assert "Estimate: 45 min" in metadata
|
||||
assert "Today after start: 15 min free" in metadata
|
||||
|
||||
|
||||
def test_escape_returns_to_the_unchanged_issue_form():
|
||||
script = f"""
|
||||
const createReview = require({json.dumps(str(MODULE))});
|
||||
|
|
|
|||
|
|
@ -105,6 +105,43 @@ process.stdout.write(JSON.stringify(updated));
|
|||
assert output["operationId"] != "1"
|
||||
|
||||
|
||||
def test_create_and_start_estimate_survives_outbox_delivery_and_completion():
|
||||
script = f"""
|
||||
const createIssueOutbox=require({json.dumps(str(OUTBOX))});
|
||||
const values=new Map();
|
||||
const storage={{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v),removeItem:k=>values.delete(k)}};
|
||||
const outbox=createIssueOutbox({{
|
||||
storage,getOwnerLogin:()=>'timmy',createOperationId:()=>'estimated-start',
|
||||
fetchJson:async()=>({{number:849,title:'Estimated work'}}),
|
||||
}});
|
||||
const queued=outbox.enqueue({{repository:'stackchain/dashboard',title:'Estimated work',
|
||||
completionIntent:'create-and-start',estimateMinutes:45}});
|
||||
(async()=>{{
|
||||
const delivered=await outbox.flush('timmy');
|
||||
process.stdout.write(JSON.stringify({{queued,completion:delivered.completions[0]}}));
|
||||
}})();
|
||||
"""
|
||||
output = run_node(script)
|
||||
|
||||
assert output["queued"]["estimateMinutes"] == 45
|
||||
assert output["completion"]["estimateMinutes"] == 45
|
||||
|
||||
|
||||
def test_editing_queued_start_estimate_rotates_the_delivery_operation():
|
||||
script = f"""
|
||||
const createIssueOutbox=require({json.dumps(str(OUTBOX))});const values=new Map();let sequence=0;
|
||||
const storage={{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v)}};
|
||||
const outbox=createIssueOutbox({{storage,getOwnerLogin:()=>'timmy',createOperationId:()=> 'op-'+(++sequence)}});
|
||||
const queued=outbox.enqueue({{repository:'o/r',title:'Sized',completionIntent:'create-and-start',estimateMinutes:30}});
|
||||
const updated=outbox.update(queued.id,{{...queued,estimateMinutes:60}});
|
||||
process.stdout.write(JSON.stringify({{queued,updated}}));
|
||||
"""
|
||||
output = run_node(script)
|
||||
|
||||
assert output["updated"]["estimateMinutes"] == 60
|
||||
assert output["updated"]["operationId"] != output["queued"]["operationId"]
|
||||
|
||||
|
||||
def test_issue_outbox_preserves_bounded_ordered_evidence_bundle():
|
||||
script = f"""
|
||||
const createIssueOutbox=require({json.dumps(str(OUTBOX))});
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user