From e68e845e7050afce1646ad5cc7034eee7f645cff Mon Sep 17 00:00:00 2001 From: hugo Date: Mon, 23 Feb 2026 21:22:15 +0100 Subject: [PATCH] fix: python scripts now work --- src/main/engine/BlogmarkTransformService.ts | 11 ++- .../DocumentationView/DocumentationView.tsx | 8 +- ...ogmarkTransformService.integration.test.ts | 80 +++++++++++++++++++ tests/engine/BlogmarkTransformService.test.ts | 64 +++++++++++++++ 4 files changed, 159 insertions(+), 4 deletions(-) create mode 100644 tests/engine/BlogmarkTransformService.integration.test.ts diff --git a/src/main/engine/BlogmarkTransformService.ts b/src/main/engine/BlogmarkTransformService.ts index 91077a0..7f65c66 100644 --- a/src/main/engine/BlogmarkTransformService.ts +++ b/src/main/engine/BlogmarkTransformService.ts @@ -181,7 +181,16 @@ _entrypoint = __bds_transform_entrypoint _transform_fn = globals().get(_entrypoint) if _transform_fn is None or not callable(_transform_fn): raise RuntimeError(f"Transform entrypoint '{_entrypoint}' is not callable") -_result = _transform_fn(_payload) +_post = _payload.get("post") +if not isinstance(_post, dict): + raise RuntimeError("Transform payload is missing a valid 'post' object") +_context = _payload.get("context") +try: + _result = _transform_fn(_post, _context) +except TypeError: + _result = _transform_fn(_post) +if _result is None: + _result = _post json.dumps(_result) `); diff --git a/src/renderer/components/DocumentationView/DocumentationView.tsx b/src/renderer/components/DocumentationView/DocumentationView.tsx index 71bdbd6..e493c6f 100644 --- a/src/renderer/components/DocumentationView/DocumentationView.tsx +++ b/src/renderer/components/DocumentationView/DocumentationView.tsx @@ -88,6 +88,8 @@ export const DocumentationView: React.FC = () => { const { t: tr } = useI18n(); const { picoTheme } = useAppStore(); const resolvedTheme = getRendererPicoTheme(picoTheme); + const CODE_COPY_DEFAULT_ICON = '\u29c9'; + const CODE_COPY_SUCCESS_ICON = '\u2713'; const scrollContainerRef = useRef(null); const articleRef = useRef(null); const headingSlugCounts = new Map(); @@ -217,9 +219,9 @@ export const DocumentationView: React.FC = () => { wrapper?.classList.add('code-copy-success'); if (icon) { - icon.textContent = '✓'; + icon.textContent = CODE_COPY_SUCCESS_ICON; setTimeout(() => { - icon.textContent = '⧉'; + icon.textContent = CODE_COPY_DEFAULT_ICON; wrapper?.classList.remove('code-copy-success'); }, 1200); } @@ -234,7 +236,7 @@ export const DocumentationView: React.FC = () => { }); }} > - + {CODE_COPY_DEFAULT_ICON}
              = {}): BlogmarkTransformInput {
+  return {
+    post: {
+      title: 'BoardGameGeek | Great Game',
+      content: 'Read this: BoardGameGeek | Great Game',
+      tags: ['inbox'],
+      categories: ['blogmark'],
+    },
+    context: {
+      source: 'blogmark',
+      url: 'https://boardgamegeek.com/boardgame/12345',
+    },
+    ...overrides,
+  };
+}
+
+function createTransformScript(overrides: Partial = {}): ScriptData {
+  return {
+    id: 'bgg-link-transform',
+    projectId: 'default',
+    slug: 'bgg_link',
+    title: 'BGG Link Transform',
+    kind: 'transform',
+    entrypoint: 'normalize_blogmark',
+    enabled: true,
+    version: 1,
+    filePath: '/tmp/bgg_link.py',
+    content: [
+      'def normalize_blogmark(post, context=None):',
+      '    title = (post.get("title") or "").strip()',
+      '    if title and "BoardGameGeek" in title:',
+      '        clean_title = title.split(" | ")[0]',
+      '        post["title"] = clean_title',
+      '        post["content"] = (post.get("content") or "").replace(title, clean_title)',
+      '    post["categories"] = ["spielelog", "asides"]',
+      '    tags = post.get("tags") or []',
+      '    tags.append("spielen")',
+      '    post["tags"] = sorted({str(tag).strip().lower() for tag in tags if str(tag).strip()})',
+      '    if context and context.get("url"):',
+      '        toast(f"BGG transform applied: {post.get(' + "'title'" + ')} @ {context.get(' + "'url'" + ')}")',
+      '    else:',
+      '        toast(f"BGG transform applied: {post.get(' + "'title'" + ')}")',
+      '    return post',
+      '',
+    ].join('\n'),
+    createdAt: '2026-02-23T00:00:00.000Z',
+    updatedAt: '2026-02-23T00:00:00.000Z',
+    ...overrides,
+  };
+}
+
+describe('BlogmarkTransformService (Pyodide integration)', () => {
+  it('executes transform scripts with real pyodide runtime and applies post mutations', async () => {
+    const provider: BlogmarkTransformScriptProvider = {
+      getScripts: async () => [createTransformScript()],
+    };
+
+    const service = new BlogmarkTransformService({ provider });
+
+    const result = await service.applyTransforms(createInput());
+
+    expect(result.errors).toEqual([]);
+    expect(result.appliedScriptIds).toEqual(['bgg-link-transform']);
+    expect(result.post.title).toBe('BoardGameGeek');
+    expect(result.post.content).toBe('Read this: BoardGameGeek');
+    expect(result.post.categories).toEqual(['spielelog', 'asides']);
+    expect(result.post.tags).toEqual(['inbox', 'spielen']);
+    expect(result.toasts).toHaveLength(1);
+    expect(result.toasts[0]).toContain('BGG transform applied: BoardGameGeek');
+    expect(result.toasts[0]).toContain('boardgamegeek.com/boardgame/12345');
+  }, 60000);
+});
diff --git a/tests/engine/BlogmarkTransformService.test.ts b/tests/engine/BlogmarkTransformService.test.ts
index 7d3be02..ebf1b63 100644
--- a/tests/engine/BlogmarkTransformService.test.ts
+++ b/tests/engine/BlogmarkTransformService.test.ts
@@ -246,4 +246,68 @@ describe('BlogmarkTransformService', () => {
 
     expect(result.toasts).toEqual(['Step finished', 'Step finished']);
   });
+
+  it('invokes python transform entrypoint with post payload shape', async () => {
+    const globalsStore = new Map();
+    const runPythonAsync = vi.fn(async (code: string) => {
+      if (code.includes('json.dumps(_result)')) {
+        const payload = JSON.parse(String(globalsStore.get('__bds_transform_payload_json')));
+
+        if (code.includes('_transform_fn(_payload)')) {
+          return JSON.stringify(payload);
+        }
+
+        return JSON.stringify({
+          ...payload.post,
+          title: 'Normalized',
+          categories: ['spielelog', 'asides'],
+          tags: ['inbox', 'spielen'],
+        });
+      }
+
+      return null;
+    });
+
+    vi.doMock('pyodide', () => ({
+      loadPyodide: vi.fn(async () => ({
+        globals: {
+          set: (key: string, value: unknown) => {
+            globalsStore.set(key, value);
+          },
+        },
+        runPythonAsync,
+      })),
+    }));
+
+    const provider: BlogmarkTransformScriptProvider = {
+      getScripts: vi.fn(async () => [
+        createScript({
+          id: 'pyodide-transform',
+          slug: 'pyodide-transform',
+          title: 'Pyodide Transform',
+          kind: 'transform',
+          entrypoint: 'normalize_blogmark',
+          content: 'def normalize_blogmark(post):\n    return post',
+        }),
+      ]),
+    };
+
+    const service = new BlogmarkTransformService({ provider });
+
+    const result = await service.applyTransforms(createInput());
+
+    const transformInvocationCode = runPythonAsync.mock.calls
+      .map((call) => call[0])
+      .find((code) => typeof code === 'string' && String(code).includes('json.dumps(_result)'));
+
+    expect(result.post.title).toBe('Normalized');
+    expect(result.post.categories).toEqual(['spielelog', 'asides']);
+    expect(result.post.tags).toEqual(['inbox', 'spielen']);
+    expect(transformInvocationCode).toBeDefined();
+    expect(String(transformInvocationCode)).not.toContain('import inspect');
+    expect(String(transformInvocationCode)).toContain('\ntry:\n');
+    expect(String(transformInvocationCode)).toContain('\nexcept TypeError:\n');
+    expect(String(transformInvocationCode)).not.toContain('\n  try:\n');
+    expect(String(transformInvocationCode)).not.toContain('\n  except TypeError:\n');
+  });
 });