feat: record browser conversion activity

This commit is contained in:
mirivlad 2026-06-29 09:41:25 +08:00
parent 8cadc957cf
commit 6fa0dd350d
3 changed files with 45 additions and 6 deletions

View File

@ -22,7 +22,8 @@
'case.selected', 'case.selected',
'browser.capture.page', 'browser.capture.page',
'browser.capture.selection', 'browser.capture.selection',
'browser.capture.link' 'browser.capture.link',
'browser.capture.converted'
]; ];
function injectStyles() { function injectStyles() {

View File

@ -70,7 +70,8 @@
"workspace.selected", "workspace.selected",
"browser.capture.page", "browser.capture.page",
"browser.capture.selection", "browser.capture.selection",
"browser.capture.link" "browser.capture.link",
"browser.capture.converted"
], ],
"handler": "recordActivityEvent" "handler": "recordActivityEvent"
} }

View File

@ -176,8 +176,10 @@ async function mountWithApi(api, props = { workspaceNode: { name: 'Project' }, w
const worklogCommand = (manifest.contributes.commands || []).find((item) => item.id === WORKLOG_COMMAND_ID); const worklogCommand = (manifest.contributes.commands || []).find((item) => item.id === WORKLOG_COMMAND_ID);
if (!worklogCommand || worklogCommand.handler !== WORKLOG_COMMAND_ID) throw new Error('activity worklog suggestion command contribution is missing'); if (!worklogCommand || worklogCommand.handler !== WORKLOG_COMMAND_ID) throw new Error('activity worklog suggestion command contribution is missing');
if (typeof api.commandHandlers.get(WORKLOG_COMMAND_ID) !== 'function') throw new Error('activity worklog suggestion command was not registered'); if (typeof api.commandHandlers.get(WORKLOG_COMMAND_ID) !== 'function') throw new Error('activity worklog suggestion command was not registered');
const activityProvider = (manifest.contributes.activityProviders || []).find((item) => item.id === 'verstak.activity.log');
if (!activityProvider || !activityProvider.events.includes('browser.capture.converted')) throw new Error('activity provider must include browser.capture.converted');
for (const name of ['file.opened', 'file.changed', 'note.saved', 'action.started', 'browser.capture.received', 'case.selected', 'browser.capture.selection']) { for (const name of ['file.opened', 'file.changed', 'note.saved', 'action.started', 'browser.capture.received', 'case.selected', 'browser.capture.selection', 'browser.capture.converted']) {
if (typeof api.handlers[name] !== 'function') throw new Error(`${name} subscription missing`); if (typeof api.handlers[name] !== 'function') throw new Error(`${name} subscription missing`);
} }
@ -210,6 +212,23 @@ async function mountWithApi(api, props = { workspaceNode: { name: 'Project' }, w
path: 'Project/Notes/Case.md', path: 'Project/Notes/Case.md',
workspaceRootPath: 'Project', workspaceRootPath: 'Project',
}, },
}, {
activityId: 'capture-1:browser.capture.converted',
type: 'browser.capture.converted',
title: 'Example Article converted',
summary: 'Project/Notes/Example_Article.md',
occurredAt: '2026-06-27T00:30:00Z',
sourcePluginId: 'verstak.browser-inbox',
workspaceRootPath: 'Project',
payload: {
captureId: 'capture-1',
conversionType: 'note',
notePath: 'Project/Notes/Example_Article.md',
workspaceRootPath: 'Project',
title: 'Example Article converted',
url: 'https://example.com/article',
sourcePluginId: 'verstak.browser-inbox',
},
}]); }]);
await api.handlers['browser.capture.selection']({ await api.handlers['browser.capture.selection']({
name: 'browser.capture.selection', name: 'browser.capture.selection',
@ -223,15 +242,33 @@ async function mountWithApi(api, props = { workspaceNode: { name: 'Project' }, w
text: 'Selected text', text: 'Selected text',
}, },
}); });
await api.handlers['browser.capture.converted']({
name: 'browser.capture.converted',
pluginId: 'verstak.browser-inbox',
timestamp: '2026-06-27T00:30:00Z',
payload: {
captureId: 'capture-1',
conversionType: 'note',
notePath: 'Project/Notes/Example_Article.md',
workspaceRootPath: 'Project',
title: 'Example Article converted',
url: 'https://example.com/article',
sourcePluginId: 'verstak.browser-inbox',
},
});
await flush(); await flush();
const stored = api.storedEvents(projectKey); const stored = api.storedEvents(projectKey);
if (stored.length !== 2) throw new Error(`expected two stored activity events, got ${stored.length}`); if (stored.length !== 3) throw new Error(`expected three stored activity events, got ${stored.length}`);
if (stored[0].type !== 'browser.capture.selection') throw new Error('stored event type mismatch'); if (stored[0].type !== 'browser.capture.selection') throw new Error('stored event type mismatch');
if (stored[0].sourcePluginId !== 'verstak.browser-inbox') throw new Error('stored event source plugin mismatch'); if (stored[0].sourcePluginId !== 'verstak.browser-inbox') throw new Error('stored event source plugin mismatch');
if (!stored.some((event) => event.type === 'browser.capture.converted' && event.title === 'Example Article converted')) {
throw new Error('conversion activity event was not stored');
}
if (api.storedEvents(globalKey).length !== 0) throw new Error('workspace activity leaked into global storage'); if (api.storedEvents(globalKey).length !== 0) throw new Error('workspace activity leaked into global storage');
if (!container.textContent.includes('Example Article')) throw new Error('browser capture title was not rendered'); if (!container.textContent.includes('Example Article')) throw new Error('browser capture title was not rendered');
if (!container.textContent.includes('browser.capture.selection')) throw new Error('event type was not rendered'); if (!container.textContent.includes('browser.capture.selection')) throw new Error('event type was not rendered');
if (!container.textContent.includes('browser.capture.converted')) throw new Error('conversion event type was not rendered');
if (!container.textContent.includes('Worklog suggestions')) throw new Error('worklog suggestions section was not rendered'); if (!container.textContent.includes('Worklog suggestions')) throw new Error('worklog suggestions section was not rendered');
if (!container.textContent.includes('Project work on 2026-06-27')) throw new Error('workspace worklog suggestion title was not rendered'); if (!container.textContent.includes('Project work on 2026-06-27')) throw new Error('workspace worklog suggestion title was not rendered');
if (!container.textContent.includes('30 min')) throw new Error('workspace worklog suggestion duration was not rendered'); if (!container.textContent.includes('30 min')) throw new Error('workspace worklog suggestion duration was not rendered');
@ -244,7 +281,7 @@ async function mountWithApi(api, props = { workspaceNode: { name: 'Project' }, w
if (suggestions[0].suggestionId !== 'worklog:Project:2026-06-27') throw new Error('worklog suggestion id mismatch'); if (suggestions[0].suggestionId !== 'worklog:Project:2026-06-27') throw new Error('worklog suggestion id mismatch');
if (suggestions[0].minutes !== 30) throw new Error(`expected 30 suggested minutes, got ${suggestions[0].minutes}`); if (suggestions[0].minutes !== 30) throw new Error(`expected 30 suggested minutes, got ${suggestions[0].minutes}`);
if (!suggestions[0].summary.includes('Example Article') || !suggestions[0].summary.includes('Saved note')) throw new Error('worklog suggestion summary did not include event titles'); if (!suggestions[0].summary.includes('Example Article') || !suggestions[0].summary.includes('Saved note')) throw new Error('worklog suggestion summary did not include event titles');
if (suggestions[0].eventIds.join(',') !== 'capture-1,note-1') throw new Error('worklog suggestion event ids mismatch'); if (suggestions[0].eventIds.join(',') !== 'capture-1,note-1,capture-1:browser.capture.converted') throw new Error('worklog suggestion event ids mismatch');
const clientView = await mountWithApi(api, { workspaceNode: { name: 'ClientA' }, workspaceRootPath: 'ClientA' }); const clientView = await mountWithApi(api, { workspaceNode: { name: 'ClientA' }, workspaceRootPath: 'ClientA' });
if (clientView.container.textContent.includes('Example Article')) throw new Error('Project activity leaked into ClientA workspace view'); if (clientView.container.textContent.includes('Example Article')) throw new Error('Project activity leaked into ClientA workspace view');
@ -296,7 +333,7 @@ async function mountWithApi(api, props = { workspaceNode: { name: 'Project' }, w
if (container.textContent.includes('Project work on 2026-06-27')) throw new Error('clear action did not remove worklog suggestions'); if (container.textContent.includes('Project work on 2026-06-27')) throw new Error('clear action did not remove worklog suggestions');
component.unmount && component.unmount(container); component.unmount && component.unmount(container);
if (api.unsubscribed.length !== 27) throw new Error(`expected 27 unsubscribers, got ${api.unsubscribed.length}`); if (api.unsubscribed.length !== 30) throw new Error(`expected 30 unsubscribers, got ${api.unsubscribed.length}`);
const persistedApi = makeApi({ const persistedApi = makeApi({
'events:workspace:Project': [{ 'events:workspace:Project': [{