deepspace 0.4.1 → 0.4.2

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.
@@ -0,0 +1,115 @@
1
+ import * as _playwright_test from '@playwright/test';
2
+ import { BrowserContext, Page, Browser } from '@playwright/test';
3
+ export { expect } from '@playwright/test';
4
+
5
+ interface MultiplayerUser {
6
+ context: BrowserContext;
7
+ page: Page;
8
+ email: string;
9
+ name: string;
10
+ /** Test account user ID, if known from the accounts registry. */
11
+ userId?: string;
12
+ }
13
+ /**
14
+ * Fixture function signature. Pass either a count (`users(2)`) or an
15
+ * array of account names (`users(['Alice', 'Bob'])`). Returns one entry
16
+ * per requested user.
17
+ */
18
+ type UsersFixture = (selector: number | string[], options?: {
19
+ label?: string;
20
+ }) => Promise<MultiplayerUser[]>;
21
+ interface MultiplayerFixtures {
22
+ users: UsersFixture;
23
+ }
24
+ declare const test: _playwright_test.TestType<_playwright_test.PlaywrightTestArgs & _playwright_test.PlaywrightTestOptions & MultiplayerFixtures, _playwright_test.PlaywrightWorkerArgs & _playwright_test.PlaywrightWorkerOptions>;
25
+
26
+ /**
27
+ * Account-credential plumbing for the testing module.
28
+ *
29
+ * Reads test accounts from `~/.deepspace/test-accounts.json` (the same
30
+ * file `deepspace test-accounts create` writes). Each account has at
31
+ * minimum `{email, password}`; `name` and `label` are optional.
32
+ *
33
+ * Used by the multi-user Playwright fixture in `./fixtures.ts` and is
34
+ * also exported standalone in case suites want to do their own
35
+ * orchestration.
36
+ */
37
+ interface TestAccount {
38
+ email: string;
39
+ password: string;
40
+ name?: string;
41
+ label?: string | null;
42
+ id?: string;
43
+ userId?: string;
44
+ createdAt?: number;
45
+ }
46
+ /**
47
+ * Load all test accounts the developer has created. Returns an empty
48
+ * array if the file doesn't exist yet.
49
+ */
50
+ declare function loadAllTestAccounts(): TestAccount[];
51
+ /**
52
+ * Pick `count` test accounts from the local registry, optionally
53
+ * filtered by `label`. Order-stable: sorted by `createdAt` ascending so
54
+ * the first test account you created is always the first one returned.
55
+ *
56
+ * Throws with a helpful message if not enough accounts exist.
57
+ */
58
+ declare function pickTestAccounts(count: number, options?: {
59
+ label?: string;
60
+ }): TestAccount[];
61
+ /**
62
+ * Find a single account by name. Used by `users(['Alice', 'Bob'])`.
63
+ * Throws if not found.
64
+ */
65
+ declare function findTestAccountByName(name: string): TestAccount;
66
+
67
+ /**
68
+ * Per-account `storageState` cache for Playwright.
69
+ *
70
+ * Better Auth applies a per-IP rate limit on `/api/auth/sign-in/email`
71
+ * (~5 sign-ins / 60s / endpoint). A multiplayer suite that spins up
72
+ * 5+ users per spec would hit the limit and start failing in non-obvious
73
+ * ways. This module signs each account in *once*, persists the
74
+ * resulting browser cookies + storage to disk, and reuses the file on
75
+ * subsequent runs.
76
+ *
77
+ * Cache layout:
78
+ * ~/.deepspace/playwright-states/<sha256(email)>.json
79
+ *
80
+ * Validity: a cached file is reused if Playwright successfully loads it
81
+ * AND the resulting context produces a non-anonymous session on the
82
+ * target app. If validation fails, we fall back to a fresh sign-in and
83
+ * overwrite the cache.
84
+ */
85
+
86
+ interface EnsureStorageStateOptions {
87
+ /** Max age of a cached state file before we re-sign-in. Default 7 days. */
88
+ maxAgeMs?: number;
89
+ /** Force a fresh sign-in even if the cache is fresh. */
90
+ force?: boolean;
91
+ }
92
+ /**
93
+ * Ensure a Playwright `storageState` file exists for `account` and
94
+ * return its path. Signs in once if the cache is missing, stale, or
95
+ * `force: true`.
96
+ */
97
+ declare function ensureStorageState(browser: Browser, account: {
98
+ email: string;
99
+ password: string;
100
+ }, baseURL: string, options?: EnsureStorageStateOptions): Promise<string>;
101
+ /**
102
+ * Convenience: open a fresh `BrowserContext` for `account` using the
103
+ * cached storage state (signing in if needed). The caller is responsible
104
+ * for closing the context.
105
+ */
106
+ declare function newSignedInContext(browser: Browser, account: {
107
+ email: string;
108
+ password: string;
109
+ }, baseURL: string, options?: EnsureStorageStateOptions): Promise<BrowserContext>;
110
+ /** Read a cached storageState file (mostly for debugging). */
111
+ declare function readCachedState(email: string): unknown | null;
112
+ /** Where the cache for `email` lives. Useful for diagnostics. */
113
+ declare function getStatePathForEmail(email: string): string;
114
+
115
+ export { type EnsureStorageStateOptions, type MultiplayerUser, type TestAccount, type UsersFixture, ensureStorageState, findTestAccountByName, getStatePathForEmail, loadAllTestAccounts, newSignedInContext, pickTestAccounts, readCachedState, test };
@@ -27,6 +27,7 @@
27
27
  "@codemirror/lang-javascript": "^6.2.2",
28
28
  "@codemirror/theme-one-dark": "^6.1.2",
29
29
  "papaparse": "^5.4.1",
30
+ "@types/papaparse": "^5.3.0",
30
31
  "docx-preview": "^0.3.5",
31
32
  "exceljs": "^4.4.0",
32
33
  "pdfjs-dist": "^4.7.76"
@@ -413,7 +413,7 @@ async function loadPdfJs(): Promise<typeof import('pdfjs-dist')> {
413
413
  pdfjsLoadPromise = (async () => {
414
414
  const [pdfjsLib, workerModule] = await Promise.all([
415
415
  import('pdfjs-dist'),
416
- // @ts-expect-error — Vite's `?worker` suffix yields a Worker constructor.
416
+ // Vite's `?worker` suffix yields a Worker constructor (typed via vite/client).
417
417
  import('pdfjs-dist/build/pdf.worker.min.mjs?worker'),
418
418
  ])
419
419
  const PdfWorker = (workerModule as { default: new () => Worker }).default
@@ -40,6 +40,7 @@ interface JsonSchemaProperty {
40
40
  minLength?: number
41
41
  maxLength?: number
42
42
  items?: JsonSchemaProperty
43
+ properties?: Record<string, JsonSchemaProperty>
43
44
  }
44
45
 
45
46
  type Catalog = Record<string, CatalogEntry[]>
@@ -1,16 +1,25 @@
1
- import { test, expect } from '@playwright/test'
2
- import { signUp } from './helpers/auth'
1
+ /**
2
+ * Integration proxy E2E — drives the /integration-test page to exercise
3
+ * the authenticated billing + proxy flow end to end.
4
+ *
5
+ * Public email/password sign-up is disabled, so the authenticated test
6
+ * signs in as a pre-created test account via the `users` fixture (from
7
+ * deepspace/testing), which handles sign-in caching, context creation,
8
+ * and cleanup. Create at least one test account first:
9
+ * npx deepspace test-accounts create --email integ@deepspace.test --password TestPass123! --name "Integration Tester"
10
+ */
11
+ import { test, expect } from 'deepspace/testing'
3
12
 
4
13
  async function waitForApp(page: import('@playwright/test').Page) {
5
14
  await page.waitForSelector('[data-testid="app-navigation"]', { timeout: 15000 })
6
15
  }
7
16
 
8
17
  test.describe('Integration API', () => {
9
- test('authenticated user can call OpenAI via integration proxy', async ({ page }) => {
10
- await signUp(page, `integ-${Date.now()}@test.local`, { name: 'Integration Tester' })
11
- await waitForApp(page)
18
+ test('authenticated user can call OpenAI via integration proxy', async ({ users }) => {
19
+ const [{ page }] = await users(1)
12
20
 
13
21
  await page.goto('/integration-test')
22
+ await waitForApp(page)
14
23
  await page.waitForSelector('[data-testid="integration-submit"]', { timeout: 10000 })
15
24
 
16
25
  // Default is openai/chat-completion — click submit
@@ -235,7 +235,7 @@ export function MessageList({ channelId, onStartDM }: MessageListProps) {
235
235
  if (actionSheet) setEditingMessageId(actionSheet.messageId)
236
236
  }}
237
237
  onDelete={() => {
238
- if (actionSheet) remove(actionSheet.messageId)
238
+ if (actionSheet) softDelete(actionSheet.messageId)
239
239
  }}
240
240
  onReply={() => {
241
241
  if (actionSheet) setThreadMessageId(actionSheet.messageId)
@@ -95,7 +95,7 @@ export default function PresenceTestPage() {
95
95
  </span>
96
96
  </div>
97
97
  <div className="text-xs text-muted-foreground space-x-2">
98
- {peer.state.cursor && (
98
+ {!!peer.state.cursor && (
99
99
  <span data-testid={`presence-peer-cursor-${peer.userId}`}>
100
100
  cursor: {(peer.state.cursor as { x: number; y: number }).x},{(peer.state.cursor as { x: number; y: number }).y}
101
101
  </span>
@@ -0,0 +1,34 @@
1
+ {
2
+ "id": "search-bar",
3
+ "name": "Search Bar",
4
+ "category": "data",
5
+ "description": "Drop-in InlineSearch/SearchOverlay for app search; use instead of raw SearchInput when showing or filtering results",
6
+ "details": "Use this before hand-rolling search over local arrays, feeds, tables, pickers, or RecordRoom records. Installs a component-only search toolkit with lightweight InlineSearch + useSearchIndex for simple in-page filtering, plus SearchOverlay for global/command search, keyboard navigation, grouped results, loading/error/empty states, click selection, optional Enter-to-submit query navigation, opt-in match highlighting, overlay-only recent result and query searches, and a debounced async-search helper. It does not add a route or demo page; import the installed component into the page you are building.",
7
+ "files": [
8
+ {
9
+ "src": "src/SearchOverlay.tsx",
10
+ "dest": "src/components/search/SearchOverlay.tsx"
11
+ }
12
+ ],
13
+ "requires": [],
14
+ "instructions": [
15
+ "Run npx deepspace add search-bar before building any search UI that filters records or displays results; it only installs src/components/search/SearchOverlay.tsx and does not add app routes.",
16
+ "Prefer InlineSearch + useSearchIndex for simple page, feed, table, list, picker, or RecordRoom filtering instead of composing SearchInput manually.",
17
+ "Use the raw SearchInput primitive only when you need a bare input with no result list, ranking, keyboard navigation, empty state, or selection handling.",
18
+ "Use InlineSearch when search belongs to a specific page section and results should appear beneath that input.",
19
+ "Use SearchOverlay when search is temporary, global, command-like, includes recent searches, submits partial text to a search-results page, or should interrupt the current workflow.",
20
+ "Pass onSearchSubmit when pressing Enter should navigate to or render a full search-results page for the typed query; result clicks still call onSelect.",
21
+ "Ask the user which placement they prefer only when both overlay and inline search are plausible and the page context does not make the placement obvious.",
22
+ "Use SearchOverlay's recentStorageKey, maxRecentItems, and showRecentSearches props when recent selections or submitted text queries should appear before the user types; keep recent searches off inline controls unless the surrounding UI has room for them.",
23
+ "Use getHighlightedParts with renderTitle, renderSubtitle, or renderDescription when matching text should be highlighted; leave highlighting to app-specific renderers so colors, emphasis, and density match the local design."
24
+ ],
25
+ "patterns": [
26
+ "For local feed/list filtering, keep the source array in the page, derive results with useSearchIndex, map results to SearchItem[], and render InlineSearch where the user expects to search.",
27
+ "For RecordRoom data, call useQuery in the page and pass records mapped to SearchItem[] into InlineSearch or SearchOverlay.",
28
+ "For overlay search that previews matching items but routes Enter to a results page, pass onSearchSubmit={(query) => navigate(`/search?q=${encodeURIComponent(query)}`)} and keep onSelect for direct result clicks.",
29
+ "Keep selections app-specific: create or update records in the onSelect handler only after the user explicitly chooses a result.",
30
+ "Use mocked or debounced async providers during prototyping before wiring live integration calls.",
31
+ "For privacy-sensitive search contexts, set showRecentSearches={false} or use a scoped recentStorageKey per page or account context."
32
+ ],
33
+ "example": "import { InlineSearch, useSearchIndex } from './components/search/SearchOverlay'\n\nconst results = useSearchIndex({\n items: posts,\n query,\n getText: (post) => [post.title, post.body, post.authorName],\n})\n\n<InlineSearch\n query={query}\n onQueryChange={setQuery}\n items={results.map((post) => ({\n id: post.id,\n title: post.title,\n subtitle: post.authorName,\n description: post.body,\n }))}\n onSelect={(item) => openPost(item.id)}\n placeholder=\"Search posts...\"\n/>"
34
+ }