fix(drafts): guard empty storage path out of the draft DB seam

This commit is contained in:
centdix
2026-06-08 16:35:58 +02:00
parent c0c29dacde
commit 6d68924959
2 changed files with 26 additions and 19 deletions
@@ -568,7 +568,7 @@ describe('global AI tools', () => {
it('lists and edits the live script editor draft through its effective path', async () => {
seedDbDraft(
'script',
'',
'u/admin/draft_amazed',
{
path: 'u/admin/amazed_script',
summary: 'Live script',
@@ -584,7 +584,7 @@ describe('global AI tools', () => {
UserDraft.setLiveEditorDraft({
workspace: WORKSPACE,
itemKind: 'script',
storagePath: '',
storagePath: 'u/admin/draft_amazed',
effectivePath: 'u/admin/amazed_script'
})
@@ -604,7 +604,7 @@ describe('global AI tools', () => {
new_string: 'return a * b'
})
expect(dbDraftValue('script', '')).toMatchObject({
expect(dbDraftValue('script', 'u/admin/draft_amazed')).toMatchObject({
path: 'u/admin/amazed_script',
content: 'export async function main(a: number, b: number) {\n\treturn a * b\n}'
})
@@ -616,7 +616,7 @@ describe('global AI tools', () => {
it('lists and writes the live flow editor draft through its effective path', async () => {
seedDbDraft(
'flow',
'',
'u/admin/draft_live_flow',
{
path: '',
summary: 'Live flow',
@@ -632,7 +632,7 @@ describe('global AI tools', () => {
UserDraft.setLiveEditorDraft({
workspace: WORKSPACE,
itemKind: 'flow',
storagePath: '',
storagePath: 'u/admin/draft_live_flow',
effectivePath: 'u/admin/live_flow'
})
@@ -652,7 +652,7 @@ describe('global AI tools', () => {
modules: JSON.stringify([{ id: 'step', value: { type: 'identity' } }])
})
expect(dbDraftValue('flow', '')).toMatchObject({
expect(dbDraftValue('flow', 'u/admin/draft_live_flow')).toMatchObject({
path: 'u/admin/live_flow',
summary: 'Updated live flow',
value: { modules: [{ id: 'step', value: { type: 'identity' } }] }
@@ -663,7 +663,7 @@ describe('global AI tools', () => {
it('writes the live raw app editor draft through its effective path', async () => {
seedDbDraft(
'raw_app',
'',
'u/admin/draft_live_app',
{
summary: 'Live app',
files: { '/src/App.tsx': 'export default function App() { return null }' },
@@ -675,7 +675,7 @@ describe('global AI tools', () => {
UserDraft.setLiveEditorDraft({
workspace: WORKSPACE,
itemKind: 'raw_app',
storagePath: '',
storagePath: 'u/admin/draft_live_app',
effectivePath: 'u/admin/live_app'
})
@@ -685,7 +685,7 @@ describe('global AI tools', () => {
content: 'export default function New() { return null }'
})
expect(dbDraftValue('raw_app', '')).toMatchObject({
expect(dbDraftValue('raw_app', 'u/admin/draft_live_app')).toMatchObject({
files: {
'/src/App.tsx': 'export default function App() { return null }',
'/src/New.tsx': 'export default function New() { return null }'
@@ -1503,7 +1503,7 @@ describe('global AI tools', () => {
it('test_run_flow uses the live flow editor test hook when the active editor matches the path', async () => {
seedDbDraft(
'flow',
'',
'u/admin/draft_live_flow_hook',
{
path: 'u/admin/live_flow',
summary: 'Live flow',
@@ -1519,7 +1519,7 @@ describe('global AI tools', () => {
UserDraft.setLiveEditorDraft({
workspace: WORKSPACE,
itemKind: 'flow',
storagePath: '',
storagePath: 'u/admin/draft_live_flow_hook',
effectivePath: 'u/admin/live_flow'
})
const testActiveFlow = vi.fn(async () => 'job-live-flow')
@@ -1545,7 +1545,7 @@ describe('global AI tools', () => {
it('test_run_flow falls back to preview when the live flow editor test hook returns undefined', async () => {
seedDbDraft(
'flow',
'',
'u/admin/draft_live_flow_fallback',
{
path: 'u/admin/live_flow_fallback',
summary: 'Live flow fallback',
@@ -1561,7 +1561,7 @@ describe('global AI tools', () => {
UserDraft.setLiveEditorDraft({
workspace: WORKSPACE,
itemKind: 'flow',
storagePath: '',
storagePath: 'u/admin/draft_live_flow_fallback',
effectivePath: 'u/admin/live_flow_fallback'
})
const testActiveFlow = vi.fn(async () => undefined)
@@ -1952,7 +1952,7 @@ describe('prepareGlobalUserMessage', () => {
UserDraft.setLiveEditorDraft({
workspace: WORKSPACE,
itemKind: 'script',
storagePath: '',
storagePath: 'f/scripts/draft_live_greeting',
effectivePath: 'f/scripts/live_greeting'
})
@@ -284,7 +284,14 @@ function resolveDraftStoragePath(
path: string
): string {
const liveDraft = UserDraft.getLiveEditorDraft(itemKind, { workspace })
if (!liveDraft) return path
// Fall back to the caller's path when there's no live editor, or when
// the live editor hasn't committed a storage path yet. An empty
// storage path must never propagate to the DB seam: it can't be a
// `draft` row key and the draft routes (`/save_draft/{kind}/{*path}`)
// 404 on an empty wildcard tail. In practice new drafts always live at
// a real `u/{user}/draft_{uuid}` path (see `/scripts/add` et al.), so
// this is defensive — but it keeps `''` out of `DraftService` outright.
if (!liveDraft || !liveDraft.storagePath) return path
if (path === liveDraft.storagePath || path === liveDraft.effectivePath)
return liveDraft.storagePath
return path
@@ -465,10 +472,10 @@ function liveEditorDraftType(kind: (typeof LIVE_EDITOR_DRAFT_KINDS)[number]): Wo
* `isLiveDraft`.
*
* This is in-memory state that the DB draft list can't represent: a brand-new
* unsaved draft has no DB row (it lives at an empty storage path), and an
* in-progress rename's effective path differs from where the draft is stored.
* Persisted draft listing comes from the backend (`includeDraftOnly` +
* `isDraft`); this only fills that in-memory gap.
* draft that hasn't been saved to the server yet has no DB row to list, and an
* in-progress rename's effective path differs from the `u/{user}/draft_{uuid}`
* path where the draft is stored. Persisted draft listing comes from the
* backend (`includeDraftOnly` + `isDraft`); this only fills that in-memory gap.
*
* Existence and path are taken straight from the live registry
* (`getLiveEditorDraft`) — not gated on the in-tab value cell, which is only