Repository navigation
Description
Written-by: Chief
A workflows/ folder that is a plain directory but cannot be listed is answered as "there are no workflows": ownFiles() swallows the readdirSync error, list() returns { ok: true, own: [] }, and save() spreads that empty list into its own ok: true. The page then shows a workspace with zero custom workflows while the file is on disk and still openable by name.
Where
core/src/host/workflow-store.js (main a754cc4ecab4122009aea2cc0b914097786bbf5d):
:78-86ownFiles()—if (!folderReal()) return []is the reviewed gate; thetry { return readdirSync(dir)… } catch { return [] }behind it swallows every error, includingEACCES/ENOENT.:119list()—ownFiles().flatMap(…)thenreturn { ok: true, presets, own, default }.:192save()— writes the file, thenreturn { ok: true, id, bytes, changed, ...list() }, so a failed listing becomesown: []inside a success.
folderReal() (:48-52) only lstats the folder to prove it is a real directory, which needs the parent's +x, not the folder's +r. That is why the two halves of one read disagree: naming a file (open) needs +x and works, while listing needs +r and is reported as "empty" rather than "unreadable".
Reproduction (real output, main a754cc4e)
workflows/ created by the store, one workflow saved, then chmod 0o311 on the folder (write+execute, no read) — a workspace restored from a backup that kept another uid's mode, or a chmod -R accident:
import { createWorkflowStore } from './core/src/host/workflow-store.js'
const store = createWorkflowStore({ dir }) // dir = <tmp>/ws2614c/workflows
store.customize({ from: 'blank', id: 'mine', name: 'Mine' })
console.log('list-before own =', store.list().own.map(x => x.id))
chmodSync(dir, 0o311)
const op = store.open('mine')
const s = store.save({ definition: op.workflow.definition })
console.log('save(unlistable) ->', JSON.stringify({ ok: s.ok, changed: s.changed, own: s.own, presets: Array.isArray(s.presets) }))
const l = store.list()
console.log('list(unlistable) ->', JSON.stringify({ ok: l.ok, own: l.own }))
console.log('open(mine) ->', JSON.stringify({ ok: op.ok, source: op.workflow.source }))
chmodSync(dir, 0o755)
console.log('list after chmod ->', store.list().own.map(x => x.id))list-before own = [ 'mine' ]
save(unlistable) -> {"ok":true,"changed":false,"own":[],"presets":true}
list(unlistable) -> {"ok":true,"own":[]}
open(mine) -> {"ok":true,"source":"workspace"}
list after chmod -> [ 'mine' ]
No warning is printed, skipped() is not called, and nothing in the answer distinguishes "no workflows" from "cannot read the folder".
Expected
An unlistable folder is a failure, not an empty result: list() answers ok: false, and a write that succeeded is never reported with a list the code could not read.
Fix (decided here, do not widen it)
ownFiles(): keepif (!folderReal()) return [], delete the try/catch so areaddirSyncfailure propagates. The "one broken file must not take the list down" rule stays where it belongs — inreadOwn()'sJSON.parsecatch, which is untouched.list(): catch that failure and answer the store's existing refusal shape —
{ ok: false, code: 'workflow/unreadable', message: 'the workflow folder <dir> is there but cannot be listed, so its workflows cannot be read', problems: [] }.save()andsetDefault(): the follow-uplist()must not be spread intook: true. When it fails, answerok: falsewithcode: 'workflow/unreadable'andwrote: true|falsepluschanged, so the caller learns both facts — the write happened, the folder cannot be listed. Never answerok: truewith an emptyown.definitions()/rt.workflowDefinitions()(:303-305) keep their preset fallback: the board read must stay green. Changing that fallback is out of scope.
Scope
In: core/src/host/workflow-store.js; a red-first test in core/test/workflow-store.test.js (the chmod 0o311 case, skipped when process.getuid?.() === 0 because root ignores the mode; assert list → ok:false + workflow/unreadable, save → ok:false with wrote:true, open still works, and everything lists again after chmod 0o755); one line in docs/subsystems/core-host.md where the workflow-store row describes the folder gate; changelog.d/2614.fixed.md; regenerate patches/ds-bot/ (node scripts/ds-bot-patches.mjs generate + check) and keep the files/ mirrors byte-identical to core/.
Out: the write-side folderWritable/fileWritable rules (#2057, #2079), the readdir/lstat kind classification, how the settings page renders the new error, and any new join(dir, …) sink. If you do add a sink, add it to WORKFLOW_PROBES and to the #2068 probe test as that test demands.
Activity
fix(host): refuse a workflow folder that cannot be listed (ThinkFlowL…
Release v0.2.26 (ThinkFlowLab#2627)