Skip to content

Commit 40b86c0

Browse files
terryyincursoragent
andcommitted
chore: remove obsolete one-shot extract-note endpoint
The frontend now uses extract-note-preview and create-extracted-note only; drop the unused POST /api/ai/extract-note/{note} and consolidate its test coverage into the preview and create controller tests. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent baa8345 commit 40b86c0

11 files changed

Lines changed: 180 additions & 279 deletions

File tree

backend/src/main/java/com/odde/doughnut/controllers/AiController.java

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -140,16 +140,6 @@ public NoteRealm createExtractedNote(
140140
return noteConstructionService.createNoteFromExtractedSuggestion(note, request);
141141
}
142142

143-
@PostMapping("/extract-note/{note}")
144-
@Transactional
145-
public NoteRealm extractNote(
146-
@PathVariable(value = "note") @Schema(type = "integer") Note note,
147-
@RequestBody NoteRefinementLayoutSelectionRequestDTO request)
148-
throws UnexpectedNoAccessRightException, JsonProcessingException {
149-
return noteConstructionService.createNoteFromExtractedSuggestion(
150-
note, extractNoteFromLayoutSelection(note, request));
151-
}
152-
153143
private NoteExtractionResult extractNoteFromLayoutSelection(
154144
Note note, NoteRefinementLayoutSelectionRequestDTO request)
155145
throws UnexpectedNoAccessRightException, JsonProcessingException {

backend/src/test/java/com/odde/doughnut/controllers/AiControllerCreateExtractedNoteTest.java

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,18 @@
77
import com.odde.doughnut.algorithms.FrontmatterAliases;
88
import com.odde.doughnut.controllers.dto.ApiError;
99
import com.odde.doughnut.controllers.dto.NoteRealm;
10+
import com.odde.doughnut.entities.Folder;
1011
import com.odde.doughnut.entities.Note;
12+
import com.odde.doughnut.entities.Notebook;
1113
import com.odde.doughnut.entities.repositories.NoteRepository;
1214
import com.odde.doughnut.exceptions.ApiException;
1315
import com.odde.doughnut.exceptions.UnexpectedNoAccessRightException;
1416
import com.odde.doughnut.services.ai.NoteExtractionResult;
1517
import org.junit.jupiter.api.BeforeEach;
1618
import org.junit.jupiter.api.Nested;
1719
import org.junit.jupiter.api.Test;
20+
import org.junit.jupiter.params.ParameterizedTest;
21+
import org.junit.jupiter.params.provider.ValueSource;
1822
import org.springframework.beans.factory.annotation.Autowired;
1923

2024
class AiControllerCreateExtractedNoteTest extends ControllerTestBase {
@@ -83,5 +87,67 @@ void shouldRejectInvalidAliasesInNewNoteContent() {
8387
makeMe.entityPersister.refresh(testNote);
8488
assertThat(testNote.getContent()).isEqualTo(originalContent);
8589
}
90+
91+
@ParameterizedTest
92+
@ValueSource(booleans = {false, true})
93+
void shouldPlaceExtractedNoteAtExpectedLocation(boolean sourceInFolder)
94+
throws UnexpectedNoAccessRightException {
95+
Note sourceNote;
96+
Folder expectedFolder = null;
97+
if (sourceInFolder) {
98+
Notebook notebook = makeMe.aNotebook().creatorAndOwner(currentUser.getUser()).please();
99+
expectedFolder = makeMe.aFolder().notebook(notebook).name("Context").please();
100+
sourceNote =
101+
makeMe
102+
.aNote()
103+
.title("Sample")
104+
.folder(expectedFolder)
105+
.content("Original content with a key suggestion to extract.")
106+
.please();
107+
} else {
108+
sourceNote = newRootNoteWithExtractableContent(makeMe, currentUser.getUser());
109+
}
110+
111+
NoteExtractionResult request =
112+
extractionResult(
113+
sourceInFolder ? "Point B" : "Extracted Note",
114+
sourceInFolder ? "Extracted" : "Expanded content for the new note.",
115+
sourceInFolder ? "A. C. D. E." : "Updated parent with summary.");
116+
NoteRealm response = controller.createExtractedNote(sourceNote, request);
117+
Note persistedNote = noteRepository.findById(response.getNote().getId()).orElseThrow();
118+
if (sourceInFolder) {
119+
assertThat(persistedNote.getFolder().getId()).isEqualTo(expectedFolder.getId());
120+
} else {
121+
assertThat(persistedNote.getFolder()).isNull();
122+
assertThat(noteRepository.findById(sourceNote.getId()).orElseThrow().getContent())
123+
.isEqualTo("Updated parent with summary.");
124+
}
125+
}
126+
127+
@Test
128+
void shouldRefreshWikiLinkCacheForOriginalAndNewNoteAfterExtraction()
129+
throws UnexpectedNoAccessRightException {
130+
Note testNote =
131+
makeMe
132+
.aNote()
133+
.title("Sample")
134+
.notebookOwnedBy(currentUser.getUser())
135+
.content("A. B. C.")
136+
.please();
137+
NoteExtractionResult request =
138+
extractionResult(
139+
"Point B",
140+
"Extracted from [[sample|the original note]].",
141+
"A. See [[point b|the extracted note]]. C.");
142+
143+
NoteRealm response = controller.createExtractedNote(testNote, request);
144+
145+
assertThat(response.getWikiTitles())
146+
.anyMatch(
147+
wikiTitle ->
148+
wikiTitle.getTargetToken().equals("sample")
149+
&& wikiTitle.getDisplayText().equals("the original note")
150+
&& wikiTitle.getNoteId().equals(testNote.getId()));
151+
}
86152
}
87153
}

backend/src/test/java/com/odde/doughnut/controllers/AiControllerExtractNotePreviewTest.java

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,24 @@
22

33
import static com.odde.doughnut.controllers.AiControllerExtractNoteTestSupport.*;
44
import static org.assertj.core.api.Assertions.assertThat;
5+
import static org.mockito.Mockito.verify;
56

67
import com.fasterxml.jackson.core.JsonProcessingException;
78
import com.odde.doughnut.entities.Note;
89
import com.odde.doughnut.entities.repositories.NoteRepository;
910
import com.odde.doughnut.exceptions.UnexpectedNoAccessRightException;
1011
import com.odde.doughnut.services.ai.NoteExtractionResult;
1112
import com.odde.doughnut.services.ai.NoteRefinementLayout;
13+
import com.odde.doughnut.services.ai.NoteRefinementLayoutItem;
1214
import com.odde.doughnut.testability.OpenAiStructuredResponseMock;
1315
import com.openai.client.OpenAIClient;
16+
import com.openai.models.responses.StructuredResponseCreateParams;
1417
import java.util.List;
18+
import java.util.Optional;
1519
import org.junit.jupiter.api.BeforeEach;
1620
import org.junit.jupiter.api.Nested;
1721
import org.junit.jupiter.api.Test;
22+
import org.mockito.ArgumentCaptor;
1823
import org.springframework.beans.factory.annotation.Autowired;
1924
import org.springframework.test.context.bean.override.mockito.MockitoBean;
2025

@@ -64,5 +69,92 @@ void shouldReturnExtractionPreviewWithoutPersisting()
6469
makeMe.entityPersister.refresh(testNote);
6570
assertThat(testNote.getContent()).isEqualTo(originalContent);
6671
}
72+
73+
private NoteRefinementLayout sampleLayout() {
74+
return new NoteRefinementLayout(
75+
List.of(
76+
new NoteRefinementLayoutItem(
77+
"p1",
78+
"Main concept",
79+
false,
80+
List.of(
81+
new NoteRefinementLayoutItem(
82+
"p1-1", "key suggestion to extract", false, List.of()))),
83+
new NoteRefinementLayoutItem("p2", "Other point", false, List.of())));
84+
}
85+
86+
@Test
87+
void shouldCallExtractNoteWithStructuredInstructions()
88+
throws UnexpectedNoAccessRightException, JsonProcessingException {
89+
Note testNote = newRootNoteWithExtractableContent(makeMe, currentUser.getUser());
90+
openAiStructuredResponseMock.stubStructuredResponse(
91+
extractionResult(
92+
"Extracted Note",
93+
"Expanded content for the new note.",
94+
"Updated parent with summary."));
95+
NoteRefinementLayout layout = sampleLayout();
96+
97+
controller.extractNotePreview(
98+
testNote, layoutSelectionRequest(layout, List.of("p1-1", "p2")));
99+
100+
@SuppressWarnings({"unchecked", "rawtypes"})
101+
ArgumentCaptor<StructuredResponseCreateParams<NoteExtractionResult>> paramsCaptor =
102+
ArgumentCaptor.forClass((Class) StructuredResponseCreateParams.class);
103+
verify(openAiStructuredResponseMock.responseService()).create(paramsCaptor.capture());
104+
StructuredResponseCreateParams<NoteExtractionResult> params = paramsCaptor.getValue();
105+
String instructions = params.rawParams().instructions().orElse("");
106+
assertThat(params.rawParams().maxOutputTokens()).isEqualTo(Optional.of(3000L));
107+
assertThat(instructions).contains("Full note layout:");
108+
assertThat(instructions).contains("\"id\" : \"p1-1\"");
109+
assertThat(instructions).contains("Selected layout item ids to extract together");
110+
assertThat(instructions).contains("[p1-1, p2]");
111+
assertThat(instructions).contains("- p1-1: \"key suggestion to extract\"");
112+
assertThat(instructions).contains("- p2: \"Other point\"");
113+
assertThat(instructions)
114+
.contains(
115+
"Prefer replacing the removed content in the original note with a natural contextual wiki link to the new note");
116+
assertThat(instructions)
117+
.contains(
118+
"Do not add YAML frontmatter or metadata properties, such as parent:, merely to backlink the new note to the original note");
119+
assertThat(instructions)
120+
.contains("Never use a generic parent property as the default extraction relationship");
121+
assertThat(instructions).contains("Wiki links are case-insensitive");
122+
assertThat(instructions).contains("[[Canonical Note Title|visible text]]");
123+
assertThat(instructions).contains("alreadyExtracted");
124+
}
125+
126+
@Test
127+
void shouldSanitizePathSeparatorsInExtractionPreview()
128+
throws UnexpectedNoAccessRightException, JsonProcessingException {
129+
Note testNote = newRootNoteWithExtractableContent(makeMe, currentUser.getUser());
130+
openAiStructuredResponseMock.stubStructuredResponse(
131+
extractionResult(
132+
"foo/bar: baz",
133+
"See [[foo/bar: baz|link]] and [[MyNb:foo/bar|nb]].",
134+
"Back to [[foo/bar: baz]]."));
135+
NoteRefinementLayout layout = layoutWithItem("p1", "key suggestion to extract");
136+
137+
NoteExtractionResult response =
138+
controller.extractNotePreview(testNote, layoutSelectionRequest(layout, List.of("p1")));
139+
140+
assertThat(response.getNewNoteTitle()).isEqualTo("foo/bar: baz");
141+
assertThat(response.getNewNoteContent())
142+
.isEqualTo("See [[foo/bar: baz|link]] and [[MyNb:foo/bar|nb]].");
143+
assertThat(response.getUpdatedOriginalNoteContent()).isEqualTo("Back to [[foo/bar: baz]].");
144+
}
145+
146+
@Test
147+
void shouldTrimSurroundingWhitespaceFromExtractionPreviewTitle()
148+
throws UnexpectedNoAccessRightException, JsonProcessingException {
149+
Note testNote = newRootNoteWithExtractableContent(makeMe, currentUser.getUser());
150+
openAiStructuredResponseMock.stubStructuredResponse(
151+
extractionResult("\u3000Extracted Note\u3000", "Expanded content.", "Updated parent."));
152+
NoteRefinementLayout layout = layoutWithItem("p1", "key suggestion to extract");
153+
154+
NoteExtractionResult response =
155+
controller.extractNotePreview(testNote, layoutSelectionRequest(layout, List.of("p1")));
156+
157+
assertThat(response.getNewNoteTitle()).isEqualTo("Extracted Note");
158+
}
67159
}
68160
}

0 commit comments

Comments
 (0)