@marcomigozzi/trailhead 0.16.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.it.md +67 -18
- package/README.md +67 -18
- package/bin/capture-action.test.js +167 -0
- package/bin/capture-docs.test.js +32 -0
- package/bin/config-dir-templates-docs.test.js +52 -0
- package/bin/decision-builds-docs.test.js +75 -0
- package/bin/engine-route-docs.test.js +148 -0
- package/bin/fog-vs-ticket-docs.test.js +56 -0
- package/bin/grill-model-docs.test.js +155 -0
- package/bin/grilling-holistic-review-docs.test.js +7 -3
- package/bin/lib/codex-projection.js +30 -14
- package/bin/lib/codex-projection.test.js +81 -17
- package/bin/lib/host-descriptor.js +15 -2
- package/bin/lib/host-descriptor.test.js +24 -0
- package/bin/lib/runtime-command-literals.test.js +184 -0
- package/bin/report-status-docs.test.js +11 -3
- package/bin/session-trust-docs.test.js +37 -0
- package/bin/surfaced-scope-docs.test.js +57 -0
- package/bin/trailhead.js +7 -5
- package/bin/trailhead.test.js +53 -11
- package/bin/unblock-docs.test.js +56 -0
- package/package.json +1 -1
- package/plugins/trailhead/.claude-plugin/plugin.json +1 -1
- package/plugins/trailhead/agents/trailhead-code-review.md +2 -2
- package/plugins/trailhead/agents/trailhead-codebase-map.md +1 -1
- package/plugins/trailhead/agents/trailhead-grill.md +41 -0
- package/plugins/trailhead/agents/trailhead-verify.md +2 -2
- package/plugins/trailhead/hooks/lib/capture.test.js +439 -0
- package/plugins/trailhead/hooks/lib/checkout-find.test.js +195 -0
- package/plugins/trailhead/hooks/lib/checkout-in.test.js +83 -7
- package/plugins/trailhead/hooks/lib/dashboard.test.js +19 -0
- package/plugins/trailhead/hooks/lib/gh-auth-preflight.js +89 -0
- package/plugins/trailhead/hooks/lib/gh-auth-preflight.test.js +132 -0
- package/plugins/trailhead/hooks/lib/inherit-session.js +17 -0
- package/plugins/trailhead/hooks/lib/issue-body.test.js +84 -0
- package/plugins/trailhead/hooks/lib/model-defaults-review.js +72 -22
- package/plugins/trailhead/hooks/lib/model-defaults-review.test.js +113 -0
- package/plugins/trailhead/hooks/lib/model-route.js +207 -0
- package/plugins/trailhead/hooks/lib/model-route.test.js +231 -0
- package/plugins/trailhead/hooks/lib/report-status.test.js +124 -2
- package/plugins/trailhead/hooks/lib/shell-scan.js +25 -0
- package/plugins/trailhead/hooks/lib/statusline-context.test.js +22 -1
- package/plugins/trailhead/hooks/lib/unmodelled.js +6 -1
- package/plugins/trailhead/hooks/mod/README.md +24 -10
- package/plugins/trailhead/hooks/mod/map-pane.tsx +124 -44
- package/plugins/trailhead/hooks/mod/my-tickets.ts +76 -0
- package/plugins/trailhead/hooks/mod/repo-pin.ts +8 -5
- package/plugins/trailhead/hooks/trailhead-check-update.js +17 -1
- package/plugins/trailhead/hooks/trailhead-check-update.test.js +60 -0
- package/plugins/trailhead/hooks/trailhead-secret-read-guard.js +40 -1
- package/plugins/trailhead/hooks/trailhead-secret-read-guard.test.js +68 -0
- package/plugins/trailhead/skills/_shared/capture-core.md +118 -0
- package/plugins/trailhead/skills/_shared/choices.md +2 -2
- package/plugins/trailhead/skills/_shared/configuration-reference.md +20 -18
- package/plugins/trailhead/skills/_shared/configuration.md +1 -1
- package/plugins/trailhead/skills/_shared/session-handoff.md +2 -2
- package/plugins/trailhead/skills/_shared/substrate-commands.md +8 -4
- package/plugins/trailhead/skills/_shared/substrate.md +1 -1
- package/plugins/trailhead/skills/_shared/surfaced-scope.md +23 -0
- package/plugins/trailhead/skills/_shared/teamwork.md +9 -5
- package/plugins/trailhead/skills/_shared/techniques/acceptance-testing.md +1 -1
- package/plugins/trailhead/skills/_shared/techniques/code-review.md +5 -2
- package/plugins/trailhead/skills/_shared/techniques/codebase-map.md +4 -1
- package/plugins/trailhead/skills/_shared/techniques/debug.md +4 -1
- package/plugins/trailhead/skills/_shared/techniques/grilling.md +26 -1
- package/plugins/trailhead/skills/_shared/techniques/research.md +4 -1
- package/plugins/trailhead/skills/_shared/techniques/verify.md +4 -2
- package/plugins/trailhead/skills/_shared/techniques.md +7 -5
- package/plugins/trailhead/skills/trailhead-capture/SKILL.md +2 -4
- package/plugins/trailhead/skills/trailhead-capture/references/capture.md +29 -4
- package/plugins/trailhead/skills/trailhead-chart/SKILL.md +1 -1
- package/plugins/trailhead/skills/trailhead-chart/references/charting.md +5 -5
- package/plugins/trailhead/skills/trailhead-chart/references/inbox.md +3 -3
- package/plugins/trailhead/skills/trailhead-manage/references/auditing.md +5 -4
- package/plugins/trailhead/skills/trailhead-work/SKILL.md +13 -11
- package/plugins/trailhead/skills/trailhead-work/references/auto.md +1 -1
- package/plugins/trailhead/skills/trailhead-work/references/ticket-engines.md +11 -10
- package/plugins/trailhead/templates/statusline-trailhead.sh +3 -3
- package/plugins/trailhead/templates/trailhead-capture.js +418 -0
- package/plugins/trailhead/templates/trailhead-checkout-find.js +256 -0
- package/plugins/trailhead/templates/trailhead-checkout-in.js +76 -20
- package/plugins/trailhead/templates/trailhead-dashboard.js +15 -2
- package/plugins/trailhead/templates/trailhead-issue-body.js +69 -6
- package/plugins/trailhead/templates/trailhead-report-status.js +123 -11
- package/plugins/trailhead/tests/map-pane-ui.test.ts +299 -37
- package/plugins/trailhead/tests/my-tickets.test.ts +66 -0
- package/plugins/trailhead/tests/repo-pin.test.ts +20 -5
- package/plugins/trailhead/types/index.d.ts +7 -0
|
@@ -15,13 +15,15 @@ const ROOT = '/repo'
|
|
|
15
15
|
const ROOT_OTHER = '/other'
|
|
16
16
|
// A linked worktree of ROOT: the repo's root is the main tree's, the checkout's own is this.
|
|
17
17
|
const ROOT_TREE = '/repo-t5'
|
|
18
|
-
// The checkout handed to a verb: base64url of the root,
|
|
18
|
+
// The checkout handed to a verb: base64url of the root, a `.`, then base64url of the pinned repo (`host/owner/name`),
|
|
19
|
+
// computed here by hand so the pane's own encoder is checked.
|
|
19
20
|
// The helper's decodeIn cannot be imported here (the CommonJS template does not load in this suite); the node suite
|
|
20
21
|
// checks decodeIn against Buffer's base64url, and these tests pin the same alphabet.
|
|
21
|
-
const
|
|
22
|
-
const b64 = btoa(Array.from(new TextEncoder().encode(
|
|
23
|
-
return
|
|
22
|
+
const b64url = (text: string) => {
|
|
23
|
+
const b64 = btoa(Array.from(new TextEncoder().encode(text), byte => String.fromCharCode(byte)).join(''))
|
|
24
|
+
return b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
|
|
24
25
|
}
|
|
26
|
+
const IN = (root: string) => `--in=${b64url(root)}.${b64url('github.com/o/r')}`
|
|
25
27
|
|
|
26
28
|
const MAP = {
|
|
27
29
|
title: 'trailhead mod',
|
|
@@ -101,6 +103,9 @@ const ticketsCall = (label: string, after?: string) => [
|
|
|
101
103
|
const ticketsListCall = (label: string) => ['gh', 'issue', 'list', '--label', label, '--state', 'open', '--limit', '200', '--json', 'number,title,labels,assignees,url']
|
|
102
104
|
const isTicketsRead = (argv: readonly string[]) => argv[1] === 'api' && argv[2] === 'graphql' && argv.includes(`query=${TICKETS_QUERY}`)
|
|
103
105
|
// The maps read only: the ticket reads are GraphQL too, but answered and failed on their own.
|
|
106
|
+
// The login and my-tickets reads (any map, the whiteboard): answered apart from the pane's own reads, recorded in `mineCalls`.
|
|
107
|
+
const isLoginRead = (argv: readonly string[]) => argv[1] === 'api' && argv[2] === 'graphql' && argv.includes('query=query{viewer{login}}')
|
|
108
|
+
const isMineRead = (argv: readonly string[]) => argv[1] === 'api' && argv[2] === 'graphql' && argv.some(a => a.startsWith('login='))
|
|
104
109
|
const isGraphql = (argv: readonly string[]) => argv[1] === 'api' && argv[2] === 'graphql' && !isTicketsRead(argv) && !isUatRead(argv) && !isCountRead(argv)
|
|
105
110
|
// The open-ticket total: one aliased `totalCount` per open map (`m<n>`) plus the whiteboard (`wb`), in one call.
|
|
106
111
|
const countQuery = (maps: number[]) =>
|
|
@@ -137,7 +142,7 @@ const stateText = (over: Record<string, unknown> = {}): string =>
|
|
|
137
142
|
})
|
|
138
143
|
|
|
139
144
|
// The plugin's atoms, which the engine resets on /clear.
|
|
140
|
-
const ATOM_KEYS = ['workState', 'isOrphaned', 'promptSnapshot', 'mapPane', 'activeMap', 'viewedMap', 'isDashboard', 'dashboardPane', 'pendingWork', 'pendingPhase', 'paneAutoOpened', 'ownTicket', 'repoPin']
|
|
145
|
+
const ATOM_KEYS = ['workState', 'isOrphaned', 'promptSnapshot', 'mapPane', 'activeMap', 'viewedMap', 'isDashboard', 'dashboardPane', 'pendingWork', 'pendingPhase', 'paneAutoOpened', 'ownTicket', 'repoPin', 'myTickets']
|
|
141
146
|
|
|
142
147
|
const PANE_PROPS = {
|
|
143
148
|
title: 'Map',
|
|
@@ -179,6 +184,9 @@ const rig = (on: On, engine?: any) => {
|
|
|
179
184
|
// The UAT read's answer: one row per open UAT ticket, `{ number, body, labels }` (the scope label rides
|
|
180
185
|
// in `labels`); null makes the read fail. `uatErrors` answers a reply with `errors`, `uatBroken` non-JSON.
|
|
181
186
|
uat: [] as unknown[] | null,
|
|
187
|
+
// My open tickets on any map or the whiteboard (the `issue()` shape); null makes the read fail.
|
|
188
|
+
mine: [] as unknown[] | null,
|
|
189
|
+
mineCalls: [] as (readonly string[])[],
|
|
182
190
|
uatErrors: false,
|
|
183
191
|
uatBroken: false,
|
|
184
192
|
dashboard: DASHBOARD as unknown[],
|
|
@@ -192,6 +200,8 @@ const rig = (on: On, engine?: any) => {
|
|
|
192
200
|
repo: { root: '/repo', remote: 'git@github.com:o/r.git', internal: false } as { root: string; remote: string | null; internal: boolean } | null,
|
|
193
201
|
// The active-map marker of the second checkout (ROOT_OTHER); null: it has none.
|
|
194
202
|
otherActive: null as string | null,
|
|
203
|
+
// Runs once before the next read of the active-map marker, to land an event inside a check.
|
|
204
|
+
beforeMarkerRead: null as (() => Promise<void>) | null,
|
|
195
205
|
// The worktree checkout's marker, what `git rev-parse --show-toplevel` answers there (null: it fails), and the
|
|
196
206
|
// session's project root; every path read and every `git` run is recorded.
|
|
197
207
|
treeActive: null as string | null,
|
|
@@ -207,6 +217,8 @@ const rig = (on: On, engine?: any) => {
|
|
|
207
217
|
calls: [] as (readonly string[])[],
|
|
208
218
|
panes: [] as { id: string; title: string; isShown: boolean; isFocused: boolean; isPlaced: boolean }[],
|
|
209
219
|
isPlaced: true,
|
|
220
|
+
// Why `ui.open` leaves the pane unplaced (the SDK's `reason`).
|
|
221
|
+
reason: 'terminal too narrow',
|
|
210
222
|
logs: [] as string[],
|
|
211
223
|
toasts: [] as string[],
|
|
212
224
|
// What a Bash call prints on stdout (the created issue's URL for a `gh issue create`).
|
|
@@ -288,13 +300,18 @@ const rig = (on: On, engine?: any) => {
|
|
|
288
300
|
if (argv[2] === 'view') return ok(JSON.stringify(isOther ? r.otherMap : r.map))
|
|
289
301
|
return ok(JSON.stringify(argv.includes('all') ? (isOther ? r.otherHistory : r.history) : isOther ? r.otherTickets : r.tickets))
|
|
290
302
|
}
|
|
291
|
-
on('fs.read', (_$, e) => {
|
|
303
|
+
on('fs.read', async (_$, e) => {
|
|
292
304
|
// A relative path reaches the hook already resolved against the host's directory, not the repo's: put
|
|
293
305
|
// it under the session's working directory, which is what the engine resolves it against. A path under
|
|
294
306
|
// one of the repos is absolute (a pinned read) and stays.
|
|
295
307
|
const isAbsolute = [ROOT, ROOT_OTHER, ROOT_TREE].some(root => e.path.startsWith(`${root}/`))
|
|
296
308
|
const path = isAbsolute ? e.path : `${r.cwd}${e.path.slice(e.path.indexOf('/.trailhead/'))}`
|
|
297
309
|
r.reads.push(path)
|
|
310
|
+
if (path.endsWith('/.trailhead/active-map') && r.beforeMarkerRead !== null) {
|
|
311
|
+
const hook = r.beforeMarkerRead
|
|
312
|
+
r.beforeMarkerRead = null
|
|
313
|
+
await hook()
|
|
314
|
+
}
|
|
298
315
|
const root = path.startsWith(`${ROOT_TREE}/`) ? ROOT_TREE : path.startsWith(`${ROOT_OTHER}/`) ? ROOT_OTHER : path.startsWith(`${ROOT}/`) ? ROOT : null
|
|
299
316
|
const marker = root === ROOT_TREE ? r.treeActive : root === ROOT_OTHER ? r.otherActive : r.active
|
|
300
317
|
const file = path.endsWith('/.trailhead/active-map') ? (root === null ? null : marker) : path.endsWith('/.trailhead/work-state.json') ? (root === null ? null : r.work) : undefined
|
|
@@ -389,6 +406,14 @@ const rig = (on: On, engine?: any) => {
|
|
|
389
406
|
const out = r.authHosts.includes(host) ? { exitCode: 0, stdout: '', stderr: '' } : { exitCode: 1, stdout: '', stderr: `You are not logged into any accounts on ${host}` }
|
|
390
407
|
return { value: { ...out, isStdoutTruncated: false, isStderrTruncated: false } }
|
|
391
408
|
}
|
|
409
|
+
if (isLoginRead(e.argv) || isMineRead(e.argv)) {
|
|
410
|
+
r.mineCalls.push([...e.argv])
|
|
411
|
+
const body = isLoginRead(e.argv)
|
|
412
|
+
? { data: { viewer: { login: 'me' } } }
|
|
413
|
+
: { data: { repository: { issues: { nodes: (r.mine ?? []).map(flatNode), pageInfo: { hasNextPage: false, endCursor: null } } } } }
|
|
414
|
+
const out = r.mine === null && isMineRead(e.argv) ? { exitCode: 1, stdout: '', stderr: 'boom' } : { exitCode: 0, stdout: JSON.stringify(body), stderr: '' }
|
|
415
|
+
return { value: { ...out, isStdoutTruncated: false, isStderrTruncated: false } }
|
|
416
|
+
}
|
|
392
417
|
r.calls.push([...e.argv])
|
|
393
418
|
r.cwds.push(e.init?.cwd)
|
|
394
419
|
// `gh` needs a repo around it: without its own cwd it uses the session's, wherever a `cd` left it.
|
|
@@ -406,7 +431,7 @@ const rig = (on: On, engine?: any) => {
|
|
|
406
431
|
...r.panes.filter(p => p.id !== e.id),
|
|
407
432
|
{ id: e.id, title: e.title ?? e.id, isShown: true, isFocused: false, isPlaced: r.isPlaced },
|
|
408
433
|
]
|
|
409
|
-
return { value: r.isPlaced ? { isPlaced: true as const } : { isPlaced: false as const, reason:
|
|
434
|
+
return { value: r.isPlaced ? { isPlaced: true as const } : { isPlaced: false as const, reason: r.reason } }
|
|
410
435
|
})
|
|
411
436
|
on('ui.close', (_$, e) => {
|
|
412
437
|
r.panes = r.panes.filter(p => p.id !== e.id)
|
|
@@ -489,7 +514,7 @@ for (const surface of SURFACES) {
|
|
|
489
514
|
expect(await textOf(ui, 'head-in-progress')).toBe('In progress · 2')
|
|
490
515
|
expect(await textOf(ui, 'head-blocked')).toBe('Blocked · 2')
|
|
491
516
|
expect((await ui.find({ key: 'head-frontier' }))?.children[0]).toMatchObject({ props: { bold: true } })
|
|
492
|
-
expect(await keysOf(ui, 'row-')).toEqual(['row-
|
|
517
|
+
expect(await keysOf(ui, 'row-')).toEqual(['row-208', 'row-212', 'row-209', 'row-210', 'row-214', 'row-211', 'row-213'])
|
|
493
518
|
|
|
494
519
|
expect(await textOf(ui, 'ticket-209')).toBe('#209 Burn-down view of the map')
|
|
495
520
|
expect(await textOf(ui, 'type-209')).toBe(' build')
|
|
@@ -543,10 +568,10 @@ for (const surface of SURFACES) {
|
|
|
543
568
|
|
|
544
569
|
expect(await textOf(ui, 'head-uat')).toBe('UAT · 3')
|
|
545
570
|
expect((await ui.find({ key: 'head-uat' }))?.children[0]).toMatchObject({ props: { bold: true } })
|
|
546
|
-
expect(await keysOf(ui, 'group-')).toEqual(['group-maps', 'group-
|
|
571
|
+
expect(await keysOf(ui, 'group-')).toEqual(['group-maps', 'group-in-progress', 'group-frontier', 'group-uat', 'group-paused', 'group-blocked'])
|
|
547
572
|
// each ticket once: a UAT row is in no other group, a paused UAT stays under Paused
|
|
548
573
|
expect(await keysOf(ui, 'row-')).toEqual([
|
|
549
|
-
'row-
|
|
574
|
+
'row-208', 'row-212', 'row-209', 'row-210', 'row-214', 'row-240', 'row-241', 'row-242', 'row-243', 'row-211', 'row-213',
|
|
550
575
|
])
|
|
551
576
|
expect(await textOf(ui, 'head-frontier')).toBe('Frontier · 3')
|
|
552
577
|
expect(await textOf(ui, 'head-paused')).toBe('Paused · 1')
|
|
@@ -647,7 +672,7 @@ for (const surface of SURFACES) {
|
|
|
647
672
|
expect(r.calls.filter(isUatRead)).toContainEqual(uatCall())
|
|
648
673
|
expect(r.calls.filter(isUatSearch)).toEqual([])
|
|
649
674
|
expect(await textOf(ui, 'head-uat')).toBe('UAT · 1')
|
|
650
|
-
expect(await keysOf(ui, 'row-')).toEqual(['row-
|
|
675
|
+
expect(await keysOf(ui, 'row-')).toEqual(['row-302', 'row-301', 'row-306', 'row-303'])
|
|
651
676
|
expect(await textOf(ui, 'uat-of-306')).toBe('tests Loose parent #290')
|
|
652
677
|
})
|
|
653
678
|
|
|
@@ -668,8 +693,8 @@ for (const surface of SURFACES) {
|
|
|
668
693
|
expect(await textOf(ui, 'head-in-progress')).toBe('In progress · 2')
|
|
669
694
|
expect(await textOf(ui, 'head-blocked')).toBe('Blocked · 2')
|
|
670
695
|
// a paused ticket appears in one group only: Frontier, In progress, Paused, Blocked in order
|
|
671
|
-
expect(await keysOf(ui, 'row-')).toEqual(['row-
|
|
672
|
-
expect(await keysOf(ui, 'group-')).toEqual(['group-maps', 'group-
|
|
696
|
+
expect(await keysOf(ui, 'row-')).toEqual(['row-208', 'row-212', 'row-209', 'row-210', 'row-214', 'row-230', 'row-231', 'row-232', 'row-211', 'row-213'])
|
|
697
|
+
expect(await keysOf(ui, 'group-')).toEqual(['group-maps', 'group-in-progress', 'group-frontier', 'group-paused', 'group-blocked'])
|
|
673
698
|
for (const n of [230, 231, 232]) {
|
|
674
699
|
expect(await hasKey(ui, `resume-${n}`)).toBe(true)
|
|
675
700
|
expect(await hasKey(ui, `work-${n}`)).toBe(false)
|
|
@@ -685,12 +710,12 @@ for (const surface of SURFACES) {
|
|
|
685
710
|
const ui = await openAndMount($, r)
|
|
686
711
|
await pressDashboard(ui, r)
|
|
687
712
|
expect(await textOf(ui, 'head-paused')).toBe('Paused · 1')
|
|
688
|
-
expect(await keysOf(ui, 'row-')).toEqual(['row-
|
|
713
|
+
expect(await keysOf(ui, 'row-')).toEqual(['row-302', 'row-301', 'row-305', 'row-303'])
|
|
689
714
|
expect(await hasKey(ui, 'work-305')).toBe(false)
|
|
690
715
|
expect(await hasKey(ui, 'resume-305')).toBe(true)
|
|
691
716
|
})
|
|
692
717
|
|
|
693
|
-
test(`needs-you tickets sit in their own group
|
|
718
|
+
test(`needs-you tickets sit in their own group right after In progress, once each, with a work button unless held [${surface}]`, async ($, on) => {
|
|
694
719
|
const r = rig(on, $)
|
|
695
720
|
r.tickets = [
|
|
696
721
|
...TICKETS,
|
|
@@ -706,9 +731,9 @@ for (const surface of SURFACES) {
|
|
|
706
731
|
|
|
707
732
|
expect(await textOf(ui, 'head-needs-you')).toBe('Needs you · 6')
|
|
708
733
|
expect((await ui.find({ key: 'head-needs-you' }))?.children[0]).toMatchObject({ props: { bold: true } })
|
|
709
|
-
expect(await keysOf(ui, 'group-')).toEqual(['group-maps', 'group-
|
|
710
|
-
// each ticket once, ahead of the frontier rows; the other counts leave them out
|
|
711
|
-
expect(await keysOf(ui, 'row-')).toEqual(['row-
|
|
734
|
+
expect(await keysOf(ui, 'group-')).toEqual(['group-maps', 'group-in-progress', 'group-needs-you', 'group-frontier', 'group-blocked'])
|
|
735
|
+
// each ticket once, after my in-progress rows and ahead of the frontier rows; the other counts leave them out
|
|
736
|
+
expect(await keysOf(ui, 'row-')).toEqual(['row-208', 'row-212', 'row-250', 'row-251', 'row-252', 'row-253', 'row-254', 'row-255', 'row-209', 'row-210', 'row-214', 'row-211', 'row-213'])
|
|
712
737
|
expect(await textOf(ui, 'head-frontier')).toBe('Frontier · 3')
|
|
713
738
|
expect(await hasKey(ui, 'head-paused')).toBe(false)
|
|
714
739
|
expect(await hasKey(ui, 'head-uat')).toBe(false)
|
|
@@ -731,16 +756,16 @@ for (const surface of SURFACES) {
|
|
|
731
756
|
expect(r.runs).toEqual([{ command: 'clear', args: '' }, { command: 'trailhead:work', args: '251' }])
|
|
732
757
|
})
|
|
733
758
|
|
|
734
|
-
test(`the dashboard view puts a needs-you whiteboard ticket
|
|
759
|
+
test(`the dashboard view puts a needs-you whiteboard ticket in its own group right after In progress [${surface}]`, async ($, on) => {
|
|
735
760
|
const r = rig(on, $)
|
|
736
761
|
r.whiteboard = [...WHITEBOARD, issue(307, 'Loose needs you', [...WB, 'trailhead:task', 'trailhead:needs-you', 'trailhead:paused'])]
|
|
737
762
|
await $.session.start(START)
|
|
738
763
|
const ui = await openAndMount($, r)
|
|
739
764
|
await pressDashboard(ui, r)
|
|
740
765
|
expect(await textOf(ui, 'head-needs-you')).toBe('Needs you · 1')
|
|
741
|
-
//
|
|
742
|
-
expect((await keysOf(ui, 'group-')).slice(0,
|
|
743
|
-
expect((await keysOf(ui, 'row-'))
|
|
766
|
+
// right after the maps group and In progress
|
|
767
|
+
expect((await keysOf(ui, 'group-')).slice(0, 3)).toEqual(['group-maps', 'group-in-progress', 'group-needs-you'])
|
|
768
|
+
expect((await keysOf(ui, 'row-')).slice(0, 2)).toEqual(['row-302', 'row-307'])
|
|
744
769
|
expect(await hasKey(ui, 'head-paused')).toBe(false)
|
|
745
770
|
expect(await hasKey(ui, 'work-307')).toBe(true)
|
|
746
771
|
expect(await hasKey(ui, 'resume-307')).toBe(false)
|
|
@@ -1119,9 +1144,9 @@ for (const surface of SURFACES) {
|
|
|
1119
1144
|
await openAndMount($, r)
|
|
1120
1145
|
expect(fetches(r)).toBe(1)
|
|
1121
1146
|
|
|
1147
|
+
// two turn ends with no write between: no flush, the trailing refresh is the only extra read
|
|
1122
1148
|
await r.clock.advance(2_000)
|
|
1123
1149
|
await completeTurn($)
|
|
1124
|
-
await $.tool.call({ tool: 'Bash', command: 'gh issue close 5' } as never)
|
|
1125
1150
|
await completeTurn($)
|
|
1126
1151
|
await settle(r)
|
|
1127
1152
|
expect(fetches(r)).toBe(1)
|
|
@@ -1138,6 +1163,37 @@ for (const surface of SURFACES) {
|
|
|
1138
1163
|
expect(fetches(r)).toBe(2)
|
|
1139
1164
|
})
|
|
1140
1165
|
|
|
1166
|
+
// A write followed by a turn end flushes: the turn end reads at once, inside the floor. The armed trailing
|
|
1167
|
+
// refresh is kept, so the read after the search index caught up still follows, one floor after the flush.
|
|
1168
|
+
test(`a write then a turn end inside the floor reads at once and keeps the trailing [${surface}]`, async ($, on) => {
|
|
1169
|
+
const r = rig(on, $)
|
|
1170
|
+
await $.session.start(START)
|
|
1171
|
+
await openAndMount($, r)
|
|
1172
|
+
expect(fetches(r)).toBe(1)
|
|
1173
|
+
|
|
1174
|
+
await r.clock.advance(2_000)
|
|
1175
|
+
await completeTurn($)
|
|
1176
|
+
await $.tool.call({ tool: 'Bash', command: 'gh issue close 5' } as never)
|
|
1177
|
+
await completeTurn($)
|
|
1178
|
+
await settle(r)
|
|
1179
|
+
expect(fetches(r)).toBe(2)
|
|
1180
|
+
// a second turn end with no write since the flush adds nothing
|
|
1181
|
+
await completeTurn($)
|
|
1182
|
+
await settle(r)
|
|
1183
|
+
expect(fetches(r)).toBe(2)
|
|
1184
|
+
|
|
1185
|
+
// the trailing one fires at the first floor's end, lands inside the flush's floor and waits for its end
|
|
1186
|
+
await r.clock.advance(FLOOR - 2_000)
|
|
1187
|
+
await settle(r)
|
|
1188
|
+
expect(fetches(r)).toBe(2)
|
|
1189
|
+
await r.clock.advance(2_000)
|
|
1190
|
+
await settle(r)
|
|
1191
|
+
expect(fetches(r)).toBe(3)
|
|
1192
|
+
await r.clock.advance(FLOOR * 2)
|
|
1193
|
+
await settle(r)
|
|
1194
|
+
expect(fetches(r)).toBe(3)
|
|
1195
|
+
})
|
|
1196
|
+
|
|
1141
1197
|
// The host drops a refused timer silently (a closed ticket stayed on the pane for good): the pane must not
|
|
1142
1198
|
// wait on a dead handle.
|
|
1143
1199
|
test(`a slow timer the host dropped is started again by the next trigger [${surface}]`, async ($, on) => {
|
|
@@ -1166,6 +1222,51 @@ for (const surface of SURFACES) {
|
|
|
1166
1222
|
expect(fetches(r)).toBe(3)
|
|
1167
1223
|
})
|
|
1168
1224
|
|
|
1225
|
+
// The trailing refresh is dropped like the slow timer (a closed ticket stayed on the pane as held): the turn
|
|
1226
|
+
// end after a write reads at once, floor or not, so the final tracker state shows with no timer involved.
|
|
1227
|
+
test(`a turn end after a write shows the close although the trailing refresh was dropped [${surface}]`, async ($, on) => {
|
|
1228
|
+
on('clock.after', { ms: FLOOR }, async () => ({ deny: 'dropped' }))
|
|
1229
|
+
const r = rig(on, $)
|
|
1230
|
+
await $.session.start(START)
|
|
1231
|
+
const ui = await openAndMount($, r)
|
|
1232
|
+
await r.clock.advance(FLOOR)
|
|
1233
|
+
// the resolution comment reads at once, before the close
|
|
1234
|
+
await $.tool.call({ tool: 'Bash', command: 'gh issue comment 209 -b done' } as never)
|
|
1235
|
+
await settle(r)
|
|
1236
|
+
r.tickets = TICKETS.filter(t => t.number !== 209)
|
|
1237
|
+
await r.clock.advance(2_000)
|
|
1238
|
+
await $.tool.call({ tool: 'Bash', command: 'gh issue close 209' } as never)
|
|
1239
|
+
await completeTurn($)
|
|
1240
|
+
await settle(r)
|
|
1241
|
+
expect(await hasKey(ui, 'row-209')).toBe(false)
|
|
1242
|
+
})
|
|
1243
|
+
|
|
1244
|
+
// Without a turn end, a pane drawn again past the dead trailing's due time and grace reads at once.
|
|
1245
|
+
test(`a pane drawn again after a dropped trailing refresh reads at once [${surface}]`, async ($, on) => {
|
|
1246
|
+
let isDropping = false
|
|
1247
|
+
on('clock.after', { ms: FLOOR }, async ($$, e, next) => (isDropping ? { deny: 'dropped' } : next(e)))
|
|
1248
|
+
const r = rig(on, $)
|
|
1249
|
+
await $.session.start(START)
|
|
1250
|
+
const ui = await openAndMount($, r)
|
|
1251
|
+
await r.clock.advance(FLOOR)
|
|
1252
|
+
isDropping = true
|
|
1253
|
+
await $.tool.call({ tool: 'Bash', command: 'gh issue comment 209 -b done' } as never)
|
|
1254
|
+
await settle(r)
|
|
1255
|
+
r.tickets = TICKETS.filter(t => t.number !== 209)
|
|
1256
|
+
await r.clock.advance(2_000)
|
|
1257
|
+
await $.tool.call({ tool: 'Bash', command: 'gh issue close 209' } as never)
|
|
1258
|
+
await settle(r)
|
|
1259
|
+
isDropping = false
|
|
1260
|
+
// nothing fired for the close: the pane still shows the closed ticket
|
|
1261
|
+
await r.clock.advance(FLOOR * 4)
|
|
1262
|
+
await settle(r)
|
|
1263
|
+
expect(await hasKey(ui, 'row-209')).toBe(true)
|
|
1264
|
+
await ui.unmount()
|
|
1265
|
+
const again = await mountPane($)
|
|
1266
|
+
await settle(r)
|
|
1267
|
+
expect(await hasKey(again, 'row-209')).toBe(false)
|
|
1268
|
+
})
|
|
1269
|
+
|
|
1169
1270
|
// The open list is a GraphQL read, never the search-backed `gh issue list --label`, which lags a write.
|
|
1170
1271
|
const listReads = (r: Rig) => r.calls.filter(argv => argv[2] === 'list' && argv.includes('--label') && argv.includes('trailhead:map-198') && argv.includes('open') && !argv.includes('trailhead:uat'))
|
|
1171
1272
|
|
|
@@ -1287,7 +1388,7 @@ for (const surface of SURFACES) {
|
|
|
1287
1388
|
await $.session.start(START)
|
|
1288
1389
|
const ui = await openAndMount($, r)
|
|
1289
1390
|
await pressDashboard(ui, r)
|
|
1290
|
-
expect(await keysOf(ui, 'row-')).toEqual(['row-
|
|
1391
|
+
expect(await keysOf(ui, 'row-')).toEqual(['row-302', 'row-301', 'row-303'])
|
|
1291
1392
|
})
|
|
1292
1393
|
|
|
1293
1394
|
test(`a write that reads at once is followed by one more read at the floor's end [${surface}]`, async ($, on) => {
|
|
@@ -1540,9 +1641,11 @@ for (const surface of SURFACES) {
|
|
|
1540
1641
|
// The token the verb receives decodes (plain base64url, the helper's alphabet) to the pinned root; the suite has no Buffer.
|
|
1541
1642
|
const [number, word] = r.runs[1].args.split(' ')
|
|
1542
1643
|
expect(number).toBe('209')
|
|
1543
|
-
expect(word).toMatch(/^--in=[A-Za-z0-9_-]+$/)
|
|
1644
|
+
expect(word).toMatch(/^--in=[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/)
|
|
1544
1645
|
expect(word).toBe(IN(ROOT))
|
|
1545
|
-
|
|
1646
|
+
const [rootPart, repoPart] = word.slice('--in='.length).split('.')
|
|
1647
|
+
expect(atob(rootPart.replace(/-/g, '+').replace(/_/g, '/'))).toBe(ROOT)
|
|
1648
|
+
expect(atob(repoPart.replace(/-/g, '+').replace(/_/g, '/').padEnd(Math.ceil(repoPart.length / 4) * 4, '='))).toBe('github.com/o/r')
|
|
1546
1649
|
expect(await ui.find({ key: 'queued-209' })).toBeUndefined()
|
|
1547
1650
|
expect(await ui.find({ key: 'work-209' })).toBeDefined()
|
|
1548
1651
|
})
|
|
@@ -2197,7 +2300,7 @@ for (const surface of SURFACES) {
|
|
|
2197
2300
|
await settle(r)
|
|
2198
2301
|
expect(await textOf(ui, 'map')).toBe('#14 older map')
|
|
2199
2302
|
expect(await textOf(ui, 'viewing-text')).toBe('viewing #14 · active #198')
|
|
2200
|
-
expect(await keysOf(ui, 'row-')).toEqual(['row-
|
|
2303
|
+
expect(await keysOf(ui, 'row-')).toEqual(['row-141', 'row-140'])
|
|
2201
2304
|
})
|
|
2202
2305
|
|
|
2203
2306
|
test(`the refresh button obeys the same floor [${surface}]`, async ($, on) => {
|
|
@@ -2295,6 +2398,20 @@ for (const surface of SURFACES) {
|
|
|
2295
2398
|
expect((await panel($)).text).toBe('Map panel closed.')
|
|
2296
2399
|
})
|
|
2297
2400
|
|
|
2401
|
+
// #321: nessuna superficie collegata (inizio sessione) non è un errore: il pannello resta aperto in attesa.
|
|
2402
|
+
test(`/trailhead:panel with no surface attached keeps the pane pending and says so plainly [${surface}]`, async ($, on) => {
|
|
2403
|
+
const r = rig(on, $)
|
|
2404
|
+
r.isPlaced = false
|
|
2405
|
+
r.reason = 'no-surface: no surface is attached ($.session.surfaces() is empty)'
|
|
2406
|
+
await $.session.start(START)
|
|
2407
|
+
const pending = 'Map panel will open as soon as the screen is ready.'
|
|
2408
|
+
expect((await panel($)).text).toBe(pending)
|
|
2409
|
+
expect(r.panes.map(p => p.id)).toEqual([PANE])
|
|
2410
|
+
// a second command while still pending is idempotent: it never closes the pane
|
|
2411
|
+
expect((await panel($)).text).toBe(pending)
|
|
2412
|
+
expect(r.panes.map(p => p.id)).toEqual([PANE])
|
|
2413
|
+
})
|
|
2414
|
+
|
|
2298
2415
|
test(`/trailhead:panel shows a pane the auto-open left undrawn instead of closing it [${surface}]`, async ($, on) => {
|
|
2299
2416
|
const r = rig(on, $)
|
|
2300
2417
|
// sessione vuota, terminale sotto la soglia dei pannelli non richiesti: l'auto-open resta non disegnato
|
|
@@ -2469,7 +2586,7 @@ for (const surface of SURFACES) {
|
|
|
2469
2586
|
|
|
2470
2587
|
expect(await textOf(ui, 'map')).toBe('#14 older map')
|
|
2471
2588
|
expect(await textOf(ui, 'destination')).toBe('An older destination.')
|
|
2472
|
-
expect(await keysOf(ui, 'row-')).toEqual(['row-
|
|
2589
|
+
expect(await keysOf(ui, 'row-')).toEqual(['row-141', 'row-140'])
|
|
2473
2590
|
expect(await textOf(ui, 'holder-141')).toBe('@carol')
|
|
2474
2591
|
expect(await textOf(ui, 'burndown-counts')).toBe('open 2 · done 0 · removed 0 · peak 2')
|
|
2475
2592
|
// the ticket in play belongs to the other map: nothing here is marked
|
|
@@ -2644,7 +2761,7 @@ for (const surface of SURFACES) {
|
|
|
2644
2761
|
await settle(r)
|
|
2645
2762
|
expect(await hasKey(ui, 'ticket-300')).toBe(false)
|
|
2646
2763
|
expect(await textOf(ui, 'map')).toBe('#14 older map')
|
|
2647
|
-
expect(await keysOf(ui, 'row-')).toEqual(['row-
|
|
2764
|
+
expect(await keysOf(ui, 'row-')).toEqual(['row-141', 'row-140'])
|
|
2648
2765
|
})
|
|
2649
2766
|
|
|
2650
2767
|
test(`with no active map the open maps are listed and one can be viewed [${surface}]`, async ($, on) => {
|
|
@@ -2674,6 +2791,57 @@ for (const surface of SURFACES) {
|
|
|
2674
2791
|
await settle(r)
|
|
2675
2792
|
}
|
|
2676
2793
|
|
|
2794
|
+
// My tickets in play on other maps and the whiteboard, under every view's In progress.
|
|
2795
|
+
const MINE = [
|
|
2796
|
+
issue(150, 'My whiteboard fix', [...WB, 'trailhead:build'], ['me']),
|
|
2797
|
+
issue(160, 'My other map ticket', ['trailhead:ticket', 'trailhead:build', 'trailhead:map-14'], ['me']),
|
|
2798
|
+
issue(161, 'My paused one', ['trailhead:ticket', 'trailhead:build', 'trailhead:map-14', 'trailhead:paused'], ['me']),
|
|
2799
|
+
issue(212, 'Native gh-backed tools', ['trailhead:ticket', 'trailhead:build', 'trailhead:map-198'], ['me']),
|
|
2800
|
+
]
|
|
2801
|
+
|
|
2802
|
+
test(`the map's In progress adds my tickets of the other maps and the whiteboard, each saying where [${surface}]`, async ($, on) => {
|
|
2803
|
+
const r = rig(on, $)
|
|
2804
|
+
r.mine = MINE
|
|
2805
|
+
await $.session.start(START)
|
|
2806
|
+
const ui = await openAndMount($, r)
|
|
2807
|
+
expect(r.mineCalls[0]).toEqual(['gh', 'api', 'graphql', '-f', 'query=query{viewer{login}}'])
|
|
2808
|
+
expect(r.mineCalls.some(argv => argv.includes('login=me'))).toBe(true)
|
|
2809
|
+
expect(await textOf(ui, 'head-in-progress')).toBe('In progress · 4')
|
|
2810
|
+
// the map's own first, then mine elsewhere; a paused one and one the map already lists are not added
|
|
2811
|
+
expect(await keysOf(ui, 'row-')).toEqual(['row-208', 'row-212', 'row-150', 'row-160', 'row-209', 'row-210', 'row-214', 'row-211', 'row-213'])
|
|
2812
|
+
expect(await textOf(ui, 'scope-of-150')).toBe('on whiteboard')
|
|
2813
|
+
expect(await textOf(ui, 'scope-of-160')).toBe('on map #14 older map')
|
|
2814
|
+
expect((await ui.find({ key: 'scope-of-160' }))?.children[0]).toMatchObject({ props: { dimColor: true } })
|
|
2815
|
+
expect(await textOf(ui, 'holder-160')).toBe('@me')
|
|
2816
|
+
expect(await hasKey(ui, 'scope-of-212')).toBe(false)
|
|
2817
|
+
expect(await hasKey(ui, 'row-161')).toBe(false)
|
|
2818
|
+
})
|
|
2819
|
+
|
|
2820
|
+
test(`the dashboard's In progress adds my tickets of every map [${surface}]`, async ($, on) => {
|
|
2821
|
+
const r = rig(on, $)
|
|
2822
|
+
r.mine = MINE
|
|
2823
|
+
await $.session.start(START)
|
|
2824
|
+
const ui = await openAndMount($, r)
|
|
2825
|
+
await pressDashboard(ui, r)
|
|
2826
|
+
expect(await textOf(ui, 'scope-of-160')).toBe('on map #14 older map')
|
|
2827
|
+
expect(await textOf(ui, 'scope-of-212')).toBe('on map #198 trailhead mod')
|
|
2828
|
+
// a whiteboard ticket of mine belongs to this view's own list, never a second time with a scope line
|
|
2829
|
+
expect(await hasKey(ui, 'scope-of-150')).toBe(false)
|
|
2830
|
+
})
|
|
2831
|
+
|
|
2832
|
+
test(`a failed read of my tickets keeps the last ones and sets no error [${surface}]`, async ($, on) => {
|
|
2833
|
+
const r = rig(on, $)
|
|
2834
|
+
r.mine = MINE
|
|
2835
|
+
await $.session.start(START)
|
|
2836
|
+
const ui = await openAndMount($, r)
|
|
2837
|
+
expect(await hasKey(ui, 'row-160')).toBe(true)
|
|
2838
|
+
r.mine = null
|
|
2839
|
+
await r.clock.advance(TIMER)
|
|
2840
|
+
await settle(r)
|
|
2841
|
+
expect(await hasKey(ui, 'row-160')).toBe(true)
|
|
2842
|
+
expect(await textOf(ui, 'age')).toBe('updated just now')
|
|
2843
|
+
})
|
|
2844
|
+
|
|
2677
2845
|
test(`the Whiteboard row shows the maps index and the whiteboard tickets [${surface}]`, async ($, on) => {
|
|
2678
2846
|
const r = rig(on, $)
|
|
2679
2847
|
await $.session.start(START)
|
|
@@ -2709,7 +2877,7 @@ for (const surface of SURFACES) {
|
|
|
2709
2877
|
expect(await textOf(ui, 'head-frontier')).toBe('Frontier · 1')
|
|
2710
2878
|
expect(await textOf(ui, 'head-in-progress')).toBe('In progress · 1')
|
|
2711
2879
|
expect(await textOf(ui, 'head-blocked')).toBe('Blocked · 1')
|
|
2712
|
-
expect(await keysOf(ui, 'row-')).toEqual(['row-
|
|
2880
|
+
expect(await keysOf(ui, 'row-')).toEqual(['row-302', 'row-301', 'row-303'])
|
|
2713
2881
|
expect(await textOf(ui, 'holder-302')).toBe('@alice')
|
|
2714
2882
|
expect(await hasKey(ui, 'work-301')).toBe(true)
|
|
2715
2883
|
expect(await textOf(ui, 'unverified')).toBe('1 unverified ticket hidden (quarantined)')
|
|
@@ -3064,7 +3232,7 @@ for (const surface of SURFACES) {
|
|
|
3064
3232
|
const ui = await openAndMount($, r)
|
|
3065
3233
|
await pressDashboard(ui, r)
|
|
3066
3234
|
expect(await textOf(ui, 'head-whiteboard')).toBe('Whiteboard · 4 open · 2 assigned')
|
|
3067
|
-
expect(await keysOf(ui, 'row-')).toEqual(['row-
|
|
3235
|
+
expect(await keysOf(ui, 'row-')).toEqual(['row-302', 'row-301', 'row-306', 'row-303'])
|
|
3068
3236
|
expect(await textOf(ui, 'holder-302')).toBe('@alice')
|
|
3069
3237
|
expect(await hasKey(ui, 'resume-306')).toBe(true)
|
|
3070
3238
|
expect(await hasKey(ui, 'row-305')).toBe(false)
|
|
@@ -3093,7 +3261,7 @@ for (const surface of SURFACES) {
|
|
|
3093
3261
|
await pressDashboard(ui, r)
|
|
3094
3262
|
await pressMap(ui, r, 14)
|
|
3095
3263
|
expect(await textOf(ui, 'map')).toBe('#14 older map')
|
|
3096
|
-
expect(await keysOf(ui, 'row-')).toEqual(['row-
|
|
3264
|
+
expect(await keysOf(ui, 'row-')).toEqual(['row-141', 'row-140'])
|
|
3097
3265
|
|
|
3098
3266
|
await pressDashboard(ui, r)
|
|
3099
3267
|
await pressMap(ui, r, 198)
|
|
@@ -3145,7 +3313,7 @@ for (const surface of SURFACES) {
|
|
|
3145
3313
|
const ui = await openAndMount($, r)
|
|
3146
3314
|
expect(await textOf(ui, 'empty')).toBe('No active map. Run /trailhead:map to pick one.')
|
|
3147
3315
|
await pressDashboard(ui, r)
|
|
3148
|
-
expect(await keysOf(ui, 'row-')).toEqual(['row-
|
|
3316
|
+
expect(await keysOf(ui, 'row-')).toEqual(['row-302', 'row-301', 'row-303'])
|
|
3149
3317
|
await ui.press({ key: 'to-map' })
|
|
3150
3318
|
await settle(r)
|
|
3151
3319
|
expect(await textOf(ui, 'empty')).toBe('No active map. Run /trailhead:map to pick one.')
|
|
@@ -3159,7 +3327,7 @@ for (const surface of SURFACES) {
|
|
|
3159
3327
|
await $.command.run({ command: 'clear', args: '' } as never)
|
|
3160
3328
|
await settle(r)
|
|
3161
3329
|
expect(await textOf(ui, 'dashboard-title')).toBe('Dashboard')
|
|
3162
|
-
expect(await keysOf(ui, 'row-')).toEqual(['row-
|
|
3330
|
+
expect(await keysOf(ui, 'row-')).toEqual(['row-302', 'row-301', 'row-303'])
|
|
3163
3331
|
})
|
|
3164
3332
|
test(`the pane keeps its repo when the session cwd moves out of it [${surface}]`, async ($, on) => {
|
|
3165
3333
|
const r = rig(on, $)
|
|
@@ -3198,7 +3366,7 @@ for (const surface of SURFACES) {
|
|
|
3198
3366
|
await completeTurn($)
|
|
3199
3367
|
await settle(r)
|
|
3200
3368
|
expect(await textOf(ui, 'map')).toBe('#14 older map')
|
|
3201
|
-
expect(await keysOf(ui, 'row-')).toEqual(['row-
|
|
3369
|
+
expect(await keysOf(ui, 'row-')).toEqual(['row-141', 'row-140'])
|
|
3202
3370
|
expect(r.calls).toContainEqual(MAPS_CALL)
|
|
3203
3371
|
expect(r.cwds.every(cwd => cwd === ROOT_OTHER)).toBe(true)
|
|
3204
3372
|
// The toplevel is read where the session now is, not at its project root, which a shell `cd` leaves behind.
|
|
@@ -3286,7 +3454,101 @@ for (const surface of SURFACES) {
|
|
|
3286
3454
|
await $.turn.start({ text: 'go', turnId: 'race-1' })
|
|
3287
3455
|
release()
|
|
3288
3456
|
await settle(r)
|
|
3289
|
-
expect(await keysOf(ui, 'row-')).toEqual(['row-
|
|
3457
|
+
expect(await keysOf(ui, 'row-')).toEqual(['row-141', 'row-140'])
|
|
3458
|
+
})
|
|
3459
|
+
|
|
3460
|
+
test(`a refresh in flight when the pin moves to another checkout of the same repo is read again [${surface}]`, async ($, on) => {
|
|
3461
|
+
const r = rig(on, $)
|
|
3462
|
+
await $.session.start(START)
|
|
3463
|
+
let release: () => void = () => {}
|
|
3464
|
+
const gate = new Promise<void>(resolve => {
|
|
3465
|
+
release = resolve
|
|
3466
|
+
})
|
|
3467
|
+
// The answer is fixed when the call starts, so the reads begun in /repo keep its tracker state.
|
|
3468
|
+
r.gh = async argv => {
|
|
3469
|
+
const answer = r.defaultGh(argv)
|
|
3470
|
+
await gate
|
|
3471
|
+
return answer
|
|
3472
|
+
}
|
|
3473
|
+
r.open()
|
|
3474
|
+
const ui = await mountPane($)
|
|
3475
|
+
await settle(r)
|
|
3476
|
+
// Another checkout of the SAME repo (same remote), with map 198 active there; the tracker changed after the
|
|
3477
|
+
// read started. A turn start refreshes the band, which moves the pin while the read is still in flight.
|
|
3478
|
+
r.cwd = ROOT_OTHER
|
|
3479
|
+
r.repo = { root: ROOT_OTHER, remote: 'git@github.com:o/r.git', internal: false }
|
|
3480
|
+
r.otherActive = '198\n'
|
|
3481
|
+
r.tickets = OTHER_TICKETS
|
|
3482
|
+
r.history = OTHER_HISTORY
|
|
3483
|
+
await $.turn.start({ text: 'go', turnId: 'race-same-repo' })
|
|
3484
|
+
release()
|
|
3485
|
+
await settle(r)
|
|
3486
|
+
await r.clock.advance(FLOOR)
|
|
3487
|
+
await settle(r)
|
|
3488
|
+
// The dropped read is repeated: the pane shows the new tracker state, not the old list.
|
|
3489
|
+
expect(await keysOf(ui, 'row-')).toEqual(['row-141', 'row-140'])
|
|
3490
|
+
})
|
|
3491
|
+
|
|
3492
|
+
test(`a same-repo pin move landing inside the stale check is read again [${surface}]`, async ($, on) => {
|
|
3493
|
+
const r = rig(on, $)
|
|
3494
|
+
await $.session.start(START)
|
|
3495
|
+
let release: () => void = () => {}
|
|
3496
|
+
const gate = new Promise<void>(resolve => {
|
|
3497
|
+
release = resolve
|
|
3498
|
+
})
|
|
3499
|
+
r.gh = async argv => {
|
|
3500
|
+
const answer = r.defaultGh(argv)
|
|
3501
|
+
await gate
|
|
3502
|
+
return answer
|
|
3503
|
+
}
|
|
3504
|
+
r.open()
|
|
3505
|
+
const ui = await mountPane($)
|
|
3506
|
+
await settle(r)
|
|
3507
|
+
// The first marker read once the gate opens is the stale check's last await: the move lands there,
|
|
3508
|
+
// after the check read the old pin and before it decides.
|
|
3509
|
+
r.beforeMarkerRead = async () => {
|
|
3510
|
+
r.cwd = ROOT_OTHER
|
|
3511
|
+
r.repo = { root: ROOT_OTHER, remote: 'git@github.com:o/r.git', internal: false }
|
|
3512
|
+
r.otherActive = '198\n'
|
|
3513
|
+
r.tickets = OTHER_TICKETS
|
|
3514
|
+
r.history = OTHER_HISTORY
|
|
3515
|
+
await $.turn.start({ text: 'go', turnId: 'race-in-check' })
|
|
3516
|
+
}
|
|
3517
|
+
release()
|
|
3518
|
+
await settle(r)
|
|
3519
|
+
await r.clock.advance(FLOOR)
|
|
3520
|
+
await settle(r)
|
|
3521
|
+
expect(await keysOf(ui, 'row-')).toEqual(['row-141', 'row-140'])
|
|
3522
|
+
})
|
|
3523
|
+
|
|
3524
|
+
test(`a dashboard read in flight when the pin moves to another checkout of the same repo is read again [${surface}]`, async ($, on) => {
|
|
3525
|
+
const r = rig(on, $)
|
|
3526
|
+
r.maps = [MAPS[0]]
|
|
3527
|
+
await $.session.start(START)
|
|
3528
|
+
const ui = await openAndMount($, r)
|
|
3529
|
+
let release: () => void = () => {}
|
|
3530
|
+
const gate = new Promise<void>(resolve => {
|
|
3531
|
+
release = resolve
|
|
3532
|
+
})
|
|
3533
|
+
r.gh = async argv => {
|
|
3534
|
+
const answer = r.defaultGh(argv)
|
|
3535
|
+
await gate
|
|
3536
|
+
return answer
|
|
3537
|
+
}
|
|
3538
|
+
// The dashboard read starts under /repo and waits on the gate.
|
|
3539
|
+
await ui.press({ key: 'open-whiteboard' })
|
|
3540
|
+
await settle(r)
|
|
3541
|
+
r.cwd = ROOT_OTHER
|
|
3542
|
+
r.repo = { root: ROOT_OTHER, remote: 'git@github.com:o/r.git', internal: false }
|
|
3543
|
+
r.otherActive = '198\n'
|
|
3544
|
+
r.whiteboard = [issue(320, 'New loose one', [...WB, 'trailhead:bug'])]
|
|
3545
|
+
await $.turn.start({ text: 'go', turnId: 'race-same-repo-dash' })
|
|
3546
|
+
release()
|
|
3547
|
+
await settle(r)
|
|
3548
|
+
await r.clock.advance(FLOOR)
|
|
3549
|
+
await settle(r)
|
|
3550
|
+
expect(await textOf(ui, 'dashboard-title')).toBe('Dashboard')
|
|
3551
|
+
expect(await keysOf(ui, 'row-')).toEqual(['row-320'])
|
|
3290
3552
|
})
|
|
3291
3553
|
|
|
3292
3554
|
test(`the pin survives /clear after the session cwd moved out of the repo [${surface}]`, async ($, on) => {
|
|
@@ -3385,7 +3647,7 @@ for (const surface of SURFACES) {
|
|
|
3385
3647
|
expect(await keysOf(ui, 'row-')).not.toContain('row-209')
|
|
3386
3648
|
await settle(r)
|
|
3387
3649
|
expect(await textOf(ui, 'map')).toBe('#14 older map')
|
|
3388
|
-
expect(await keysOf(ui, 'row-')).toEqual(['row-
|
|
3650
|
+
expect(await keysOf(ui, 'row-')).toEqual(['row-141', 'row-140'])
|
|
3389
3651
|
})
|
|
3390
3652
|
|
|
3391
3653
|
// The panel follows the whiteboard ticket this session takes (#306).
|