Files
windmill/frontend/src/lib/userDraftSanitize.test.ts
T
Ruben FiszelandClaude Opus 4.8 3371265382 fix(frontend): ignore hash/assets in script diffs and drafts (WIN-2071) (#9664)
* fix(frontend): ignore hash/assets in script diffs and drafts

The script editor's draft value is seeded from the full `getScriptByPath`
DB row (since #9351), so it carries `hash` (the deployed version's
identity) and `assets` (re-derived from the script content by the
editor). Neither is editable draft content, yet both were persisted into
the draft row and surfaced as spurious changes in the workspace/fork
compare diff view.

- Add `hash`, `assets`, and the read-time-computed `inherited_labels` to
  `CLEANED_VALUE_KEYS` so the shared diff/unsaved-change strip ignores
  them everywhere (DiffDrawer + WorkspaceItemDiffViewer).
- Strip `hash` and `assets` from script drafts at the single persistence
  chokepoint (`UserDraftDbSyncer.save`) so every path — reactive
  autosave, Ctrl/Cmd+S flush, the pagehide keepalive — sends the same
  trimmed payload. On reload the deployed row re-supplies them.

Fixes WIN-2071

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(frontend): clarify draft sanitizer vs diff-strip relationship

The two field lists are intentionally not equal — only the hash/assets
overlap must stay consistent. Reword the comment so a future maintainer
doesn't add keys to one expecting parity with the other.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 11:21:24 +00:00

55 lines
2.0 KiB
TypeScript

import { describe, it, expect, vi } from 'vitest'
// Importing the syncer pulls in the generated client and its localStorage /
// pagehide wiring; stub the same surfaces the other syncer test does so the
// pure-function import stays side-effect free.
vi.mock('./gen', () => ({
DraftService: { updateDraft: vi.fn() }
}))
vi.mock('./gen/core/OpenAPI', () => ({ OpenAPI: { BASE: '' } }))
vi.mock('./localDraftHints.svelte', () => ({ setLocalDraftHint: vi.fn() }))
import { sanitizeDraftValueForSave } from './userDraftDbSyncer.svelte'
describe('sanitizeDraftValueForSave', () => {
it('strips hash and assets from a script draft', () => {
const value = {
path: 'u/me/foo',
summary: 'hi',
content: 'export function main() {}',
hash: '123456789',
assets: [{ path: 's3://bucket/x', kind: 's3object' }]
}
const cleaned = sanitizeDraftValueForSave('script', value) as any
expect(cleaned).not.toHaveProperty('hash')
expect(cleaned).not.toHaveProperty('assets')
expect(cleaned.content).toBe('export function main() {}')
expect(cleaned.summary).toBe('hi')
})
it('does not mutate the input object', () => {
const value = { path: 'u/me/foo', hash: 'h', assets: [] }
sanitizeDraftValueForSave('script', value)
expect(value).toHaveProperty('hash', 'h')
expect(value).toHaveProperty('assets')
})
it('returns the same reference when no omitted field is present', () => {
const value = { path: 'u/me/foo', summary: 'hi' }
expect(sanitizeDraftValueForSave('script', value)).toBe(value)
})
it('leaves non-script kinds untouched even if they carry hash/assets', () => {
const value = { hash: 'h', assets: [] }
expect(sanitizeDraftValueForSave('flow', value)).toBe(value)
expect(sanitizeDraftValueForSave('app', value)).toBe(value)
})
it('passes through null (the delete signal) and non-objects', () => {
expect(sanitizeDraftValueForSave('script', null)).toBeNull()
expect(sanitizeDraftValueForSave('script', 'hello')).toBe('hello')
const arr = [1, 2, 3]
expect(sanitizeDraftValueForSave('script', arr)).toBe(arr)
})
})