feat: convert completed todos into journal entries

This commit is contained in:
2026-07-10 20:49:15 +08:00
parent 31c93bdf5b
commit 16e1407440
4 changed files with 153 additions and 13 deletions
+52
View File
@@ -257,6 +257,56 @@ function byData(container, attr, value) {
await flush();
if (api.storedEntries(projectKey).length !== 1) throw new Error('journal entry was not deleted');
const completedTodo = {
id: 'todo:Project:project-review',
title: 'Prepare project review',
description: 'Collect factual review notes.',
workspaceRootPath: 'Project',
completedAt: '2026-06-27T11:15:00.000Z',
};
const todoView = await mountWithApi(api, {
workspaceNode: { name: 'Project' },
workspaceRootPath: 'Project',
toolRequest: { type: 'completed-todo', todo: completedTodo },
});
if (!todoView.container.textContent.includes('Create journal entry from completed todo')) {
throw new Error('completed Todo did not open the Journal conversion form');
}
if (!byData(todoView.container, 'data-journal-todo', completedTodo.id)) {
throw new Error('completed Todo context was not shown in the Journal form');
}
if (byData(todoView.container, 'data-journal-input', 'title').value !== completedTodo.title) {
throw new Error('completed Todo Journal form did not prefill the exact title');
}
if (byData(todoView.container, 'data-journal-input', 'summary').value !== completedTodo.description) {
throw new Error('completed Todo Journal form did not prefill the exact description');
}
if (byData(todoView.container, 'data-journal-input', 'minutes').value !== '0') {
throw new Error('completed Todo Journal form must not invent a duration');
}
byData(todoView.container, 'data-journal-input', 'title').value = 'Prepare project review for handoff';
byData(todoView.container, 'data-journal-input', 'summary').value = 'Reviewed factual project notes before handoff.';
byData(todoView.container, 'data-journal-action', 'save-entry').click();
await flush();
if (api.storedEntries(projectKey).length !== 2) throw new Error('completed Todo Journal entry was not saved');
const todoEntry = api.storedEntries(projectKey).find((entry) => entry.sourceTodoId === completedTodo.id);
if (!todoEntry) throw new Error('completed Todo reference was not stored on the Journal entry');
if (todoEntry.title !== 'Prepare project review for handoff' || todoEntry.summary !== 'Reviewed factual project notes before handoff.') {
throw new Error('completed Todo Journal form did not preserve the user-edited fields');
}
const duplicateTodoView = await mountWithApi(api, {
workspaceNode: { name: 'Project' },
workspaceRootPath: 'Project',
toolRequest: { type: 'completed-todo', todo: completedTodo },
});
byData(duplicateTodoView.container, 'data-journal-action', 'save-entry').click();
await flush();
if (api.storedEntries(projectKey).filter((entry) => entry.sourceTodoId === completedTodo.id).length !== 1) {
throw new Error('completed Todo Journal conversion created a duplicate entry');
}
const globalView = await mountWithApi(api, {});
if (!globalView.container.textContent.includes('Review research capture') && !globalView.container.textContent.includes('Draft brief updated')) {
throw new Error('global journal did not aggregate remaining entries');
@@ -264,6 +314,8 @@ function byData(container, attr, value) {
component.unmount && component.unmount(container);
component.unmount && component.unmount(candidateView.container);
component.unmount && component.unmount(todoView.container);
component.unmount && component.unmount(duplicateTodoView.container);
component.unmount && component.unmount(globalView.container);
console.log('journal plugin smoke passed');
+32 -5
View File
@@ -116,16 +116,25 @@ function makeDocument() {
};
}
function loadComponent(document) {
function FakeCustomEvent(type, options = {}) {
this.type = type;
this.detail = options.detail;
}
function loadComponent(document, emittedEvents) {
const registry = {};
const window = {
VerstakPluginRegister(pluginId, bundle) {
registry[pluginId] = bundle.components || {};
},
dispatchEvent(event) {
emittedEvents.push(event);
},
};
window.window = window;
window.document = document;
vm.runInNewContext(source, { console, Date, Math, document, window }, { filename: sourcePath });
window.CustomEvent = FakeCustomEvent;
vm.runInNewContext(source, { console, Date, Math, CustomEvent: FakeCustomEvent, document, window }, { filename: sourcePath });
const component = registry['verstak.todo'] && registry['verstak.todo'].TodoView;
if (!component) throw new Error('TodoView was not registered');
return component;
@@ -158,12 +167,12 @@ async function flush() {
for (let index = 0; index < 10; index += 1) await Promise.resolve();
}
async function mountWithApi(apiState, props, document = makeDocument()) {
const component = loadComponent(document);
async function mountWithApi(apiState, props, emittedEvents = [], document = makeDocument()) {
const component = loadComponent(document, emittedEvents);
const container = new FakeNode('div');
component.mount(container, props, apiState.api);
await flush();
return { component, container, document };
return { component, container, document, emittedEvents };
}
(async () => {
@@ -208,6 +217,24 @@ async function mountWithApi(apiState, props, document = makeDocument()) {
throw new Error('mark done did not persist completed state');
}
if (!byData(container, 'data-todo-action', 'reopen')) throw new Error('done Todo did not expose reopen action');
if (!byData(container, 'data-todo-action', 'create-journal-entry')) throw new Error('done workspace Todo did not expose Journal conversion');
byData(container, 'data-todo-action', 'create-journal-entry').click();
const journalEvent = workspaceView.emittedEvents.find((event) => event.type === 'verstak:workspace-open-tool');
if (!journalEvent || !journalEvent.detail || journalEvent.detail.kind !== 'journal') {
throw new Error('Todo Journal conversion did not request the Journal workspace tool');
}
const journalRequest = journalEvent.detail.toolRequest;
if (!journalRequest || journalRequest.type !== 'completed-todo' || !journalRequest.todo) {
throw new Error('Todo Journal conversion did not send a completed-todo request');
}
if (journalRequest.todo.id !== createdTodo.id
|| journalRequest.todo.title !== 'Prepare project review updated'
|| journalRequest.todo.description !== 'Collect factual review notes.'
|| journalRequest.todo.workspaceRootPath !== 'Project'
|| !journalRequest.todo.completedAt) {
throw new Error('Todo Journal conversion did not preserve factual completed Todo fields');
}
apiState.settings['todos:global'].push({
id: 'todo-client',