definition.client.spec.ts 4.6 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
  1. /**
  2. * What the `text` type claims, and how it yields.
  3. *
  4. * The type is the fallback viewer for every Session-scoped `file` resource address, so the contract
  5. * worth asserting is the yielding: a narrower type registered at a higher
  6. * band takes its addresses, and everything else still lands here. Routing is
  7. * exercised through the real registry, because "fallback" means whatever the
  8. * registry's ranking means by it.
  9. */
  10. import { describe, expect, it } from 'vitest'
  11. import { Context } from '@deepseek-ai/cordis'
  12. import { sessionFileAddress } from '@deepseek-ai/dsh-util-workspace-path'
  13. import { SidebarRightTabRegistry } from '@deepseek-ai/dsh-client-ui-sidebar-right/src/client/tab-registry.ts'
  14. import { TEXTPREVIEW_ID, TEXTPREVIEW_KIND, basenameOf, textDefinition } from '../src/client/definition.ts'
  15. describe('basenameOf', () => {
  16. it('decodes the last segment, so an escaped name reads as itself', () => {
  17. expect(basenameOf('dsh-resource://file/session/s-1/work/notes/a%20b%23c.md')).toBe('a b#c.md')
  18. })
  19. it('falls back to the whole address when there is no last segment', () => {
  20. expect(basenameOf('dsh-resource://file/session/s-1/')).toBe('dsh-resource://file/session/s-1/')
  21. })
  22. it('keeps a malformed percent escape as it is rather than refusing the address', () => {
  23. expect(basenameOf('dsh-resource://file/session/s-1/work/%E0%A4%A')).toBe('%E0%A4%A')
  24. })
  25. })
  26. describe('textDefinition', () => {
  27. it('is the fallback claimant of every Session file address, titled by basename', () => {
  28. const definition = textDefinition()
  29. expect(definition.id).toBe(TEXTPREVIEW_ID)
  30. expect(definition.kind).toBe(TEXTPREVIEW_KIND)
  31. expect(definition.patterns).toEqual(['dsh-resource://file/**'])
  32. expect(definition.priority).toBe('fallback')
  33. expect(definition.title('dsh-resource://file/session/s-1/work/README.md')).toBe('README.md')
  34. // Reads require an addressed Session, even when the file path is absolute.
  35. expect(definition.canOpen?.('dsh-resource://file/session/s-1/work/README.md')).toBe(true)
  36. expect(definition.canOpen?.(sessionFileAddress('s-1', '/home/me/README.md'))).toBe(true)
  37. expect(definition.canOpen?.('dsh-resource://file/absolute/home/me/README.md')).toBe(false)
  38. expect(definition.canOpen?.('dsh-resource://file/shared/team/README.md')).toBe(false)
  39. expect(definition.canOpen?.('dsh-resource://file/session')).toBe(false)
  40. })
  41. })
  42. describe('text type in the registry', () => {
  43. function registry() {
  44. const tabs = new SidebarRightTabRegistry(new Context())
  45. tabs.register(textDefinition())
  46. return tabs
  47. }
  48. it('claims files of any extension, depth, and dot-prefix with relative or absolute Session paths', () => {
  49. const tabs = registry()
  50. for (const address of [
  51. 'dsh-resource://file/session/s-1/a.md',
  52. 'dsh-resource://file/session/s-1/deep/er/path/x.py',
  53. 'dsh-resource://file/session/s-1/w/.env',
  54. 'dsh-resource://file/session/s-1/w/Makefile',
  55. sessionFileAddress('s-1', '/home/me/notes.md'),
  56. sessionFileAddress('s-1', 'C:/w/x.ts'),
  57. sessionFileAddress('s-1', '//host/share/x.ts'),
  58. ]) {
  59. expect(tabs.claim(address)).toEqual({ kind: TEXTPREVIEW_KIND, contentId: address, title: basenameOf(address) })
  60. }
  61. })
  62. it.each([
  63. 'dsh-resource://file/shared/team/notes.md',
  64. 'dsh-resource://file/absolute/home/me/notes.md',
  65. 'dsh-resource://file/absolute/C:/w/notes.md',
  66. ])('refuses a file address without a Session at claim time, named or ranked: %s', (shared) => {
  67. const tabs = registry()
  68. expect(tabs.candidates(shared)).toEqual([])
  69. expect(() => tabs.claim(shared)).toThrow('no registered tab type claims')
  70. expect(() => tabs.claim(shared, TEXTPREVIEW_KIND)).toThrow(`tab type "${TEXTPREVIEW_KIND}" refuses`)
  71. })
  72. it('yields an address to a narrower type at the extension band, and keeps the rest', () => {
  73. const tabs = registry()
  74. tabs.register({ id: 'test/image', kind: 'image', patterns: ['*.png'], priority: 'extension', title: () => 'image' })
  75. expect(tabs.claim('dsh-resource://file/session/s-1/w/logo.png').kind).toBe('image')
  76. expect(tabs.claim('dsh-resource://file/session/s-1/w/logo.md').kind).toBe(TEXTPREVIEW_KIND)
  77. // Still listed for the picture: a caller naming the kind may open it as text.
  78. expect(tabs.candidates('dsh-resource://file/session/s-1/w/logo.png').map(type => type.kind)).toEqual(['image', TEXTPREVIEW_KIND])
  79. })
  80. it('does not claim addresses of other schemes', () => {
  81. const tabs = registry()
  82. expect(() => tabs.claim('sidebar://guide')).toThrow('no registered tab type claims')
  83. expect(() => tabs.claim('https://example.com/a.md')).toThrow('no registered tab type claims')
  84. })
  85. })