1616 * - empty-turn export fails loudly (codex review hardening)
1717 */
1818
19- import { describe , it , expect , afterEach } from "vitest" ;
19+ import { describe , it , expect } from "vitest" ;
2020import path from "path" ;
21- import { mkdir , mkdtemp , rm , readdir , readFile , writeFile , copyFile } from "fs/promises" ;
22- import { tmpdir } from "os" ;
23- import { runCLI , expectCLIExit , expectCLIFailure } from "./fixtures/run-cli.js" ;
21+ import { mkdir , readdir , readFile , writeFile , copyFile } from "fs/promises" ;
22+ import {
23+ runCLI ,
24+ expectCLIExit ,
25+ expectCLIFailure ,
26+ type CLIResult ,
27+ } from "./fixtures/run-cli.js" ;
28+ import { useIngestWorkspaces } from "./fixtures/ingest-workspace.js" ;
2429
2530const FIXTURES = path . resolve ( "test/fixtures/sessions" ) ;
2631const CLAUDE_FIXTURE = path . join ( FIXTURES , "claude-session.jsonl" ) ;
@@ -29,21 +34,35 @@ const CURSOR_FIXTURE = path.join(FIXTURES, "cursor-session.json");
2934const MALFORMED_FIXTURE = path . join ( FIXTURES , "malformed.jsonl" ) ;
3035const EMPTY_TURNS_FIXTURE = path . join ( FIXTURES , "claude-empty-session.jsonl" ) ;
3136
32- const tempDirs : string [ ] = [ ] ;
37+ // Reuse the shared workspace lifecycle from the existing ingest tests so
38+ // the tempDirs + afterEach boilerplate stays in one place.
39+ const workspaces = useIngestWorkspaces ( "session" ) ;
3340
34- afterEach ( async ( ) => {
35- while ( tempDirs . length > 0 ) {
36- const dir = tempDirs . pop ( ) ;
37- if ( dir ) await rm ( dir , { recursive : true , force : true } ) ;
38- }
39- } ) ;
41+ /** Make a temp workspace ready for `ingest-session`. */
42+ async function makeWorkspace ( ) : Promise < string > {
43+ return workspaces . makeEmptyWorkspace ( ) ;
44+ }
45+
46+ /** Read sources/ tolerating ENOENT when the CLI failed before creating it. */
47+ async function readSources ( cwd : string ) : Promise < string [ ] > {
48+ return readdir ( path . join ( cwd , "sources" ) ) . catch ( ( ) => [ ] as string [ ] ) ;
49+ }
4050
41- /** Make a temp workspace with an empty `sources/`. */
42- async function makeWorkspace ( prefix : string ) : Promise < string > {
43- const cwd = await mkdtemp ( path . join ( tmpdir ( ) , `llmwiki-session-${ prefix } -` ) ) ;
44- tempDirs . push ( cwd ) ;
45- await mkdir ( path . join ( cwd , "sources" ) , { recursive : true } ) ;
46- return cwd ;
51+ /**
52+ * Common assertion shape for tests that expect the CLI to fail with a
53+ * specific stderr pattern AND verify no markdown was written into
54+ * sources/. Hoisted so the empty-turns and empty-slug tests share it
55+ * (fallow's CI mode flagged the pattern as a clone group otherwise).
56+ */
57+ async function expectIngestSessionFailureWithoutWrite (
58+ result : CLIResult ,
59+ stderrPattern : RegExp ,
60+ cwd : string ,
61+ ) : Promise < void > {
62+ expectCLIFailure ( result ) ;
63+ expect ( result . stderr . toLowerCase ( ) ) . toMatch ( stderrPattern ) ;
64+ const files = await readSources ( cwd ) ;
65+ expect ( files ) . not . toContain ( ".md" ) ;
4766}
4867
4968/**
@@ -61,46 +80,46 @@ async function assertSingleSessionIngested(cwd: string, adapterName: string): Pr
6180
6281describe ( "ingest-session CLI integration" , ( ) => {
6382 it ( "ingest-session --help shows the command description" , async ( ) => {
64- const cwd = await makeWorkspace ( "help" ) ;
83+ const cwd = await makeWorkspace ( ) ;
6584 const result = await runCLI ( [ "ingest-session" , "--help" ] , cwd ) ;
6685 expectCLIExit ( result , 0 ) ;
6786 expect ( result . stdout ) . toContain ( "ingest-session" ) ;
6887 expect ( result . stdout ) . toContain ( "session" ) ;
6988 } ) ;
7089
7190 it ( "ingest-session with claude fixture writes markdown to sources/" , async ( ) => {
72- const cwd = await makeWorkspace ( "claude" ) ;
91+ const cwd = await makeWorkspace ( ) ;
7392 const result = await runCLI ( [ "ingest-session" , CLAUDE_FIXTURE ] , cwd ) ;
7493 expectCLIExit ( result , 0 ) ;
7594 expect ( result . stdout ) . toContain ( "claude" ) ;
7695 await assertSingleSessionIngested ( cwd , "claude" ) ;
7796 } ) ;
7897
7998 it ( "ingest-session with codex fixture writes markdown to sources/" , async ( ) => {
80- const cwd = await makeWorkspace ( "codex" ) ;
99+ const cwd = await makeWorkspace ( ) ;
81100 const result = await runCLI ( [ "ingest-session" , CODEX_FIXTURE ] , cwd ) ;
82101 expectCLIExit ( result , 0 ) ;
83102 expect ( result . stdout ) . toContain ( "codex" ) ;
84103 await assertSingleSessionIngested ( cwd , "codex" ) ;
85104 } ) ;
86105
87106 it ( "ingest-session with cursor fixture writes markdown to sources/" , async ( ) => {
88- const cwd = await makeWorkspace ( "cursor" ) ;
107+ const cwd = await makeWorkspace ( ) ;
89108 const result = await runCLI ( [ "ingest-session" , CURSOR_FIXTURE ] , cwd ) ;
90109 expectCLIExit ( result , 0 ) ;
91110 expect ( result . stdout ) . toContain ( "cursor" ) ;
92111 await assertSingleSessionIngested ( cwd , "cursor" ) ;
93112 } ) ;
94113
95114 it ( "ingest-session with malformed JSONL exits non-zero with actionable error" , async ( ) => {
96- const cwd = await makeWorkspace ( "malformed" ) ;
115+ const cwd = await makeWorkspace ( ) ;
97116 const result = await runCLI ( [ "ingest-session" , MALFORMED_FIXTURE ] , cwd ) ;
98117 expectCLIFailure ( result ) ;
99118 expect ( result . stderr . toLowerCase ( ) ) . toMatch ( / m a l f o r m e d | l i n e \d + | i n v a l i d / ) ;
100119 } ) ;
101120
102121 it ( "ingest-session with unknown format exits non-zero with no-adapter message" , async ( ) => {
103- const cwd = await makeWorkspace ( "unknown" ) ;
122+ const cwd = await makeWorkspace ( ) ;
104123 const unknownFile = path . join ( cwd , "unknown.txt" ) ;
105124 await writeFile ( unknownFile , "hello world" , "utf-8" ) ;
106125 const result = await runCLI ( [ "ingest-session" , unknownFile ] , cwd ) ;
@@ -109,7 +128,7 @@ describe("ingest-session CLI integration", () => {
109128 } ) ;
110129
111130 it ( "ingest-session with missing path exits non-zero with file-not-found error" , async ( ) => {
112- const cwd = await makeWorkspace ( "missing" ) ;
131+ const cwd = await makeWorkspace ( ) ;
113132 const missingPath = path . join ( cwd , "does-not-exist.jsonl" ) ;
114133 const result = await runCLI ( [ "ingest-session" , missingPath ] , cwd ) ;
115134 expectCLIFailure ( result ) ;
@@ -119,19 +138,19 @@ describe("ingest-session CLI integration", () => {
119138
120139describe ( "ingest-session — adapter validation hardening" , ( ) => {
121140 it ( "session with no user/assistant turns fails loudly even when shape detection passes" , async ( ) => {
122- const cwd = await makeWorkspace ( "empty-turns" ) ;
141+ const cwd = await makeWorkspace ( ) ;
123142 const result = await runCLI ( [ "ingest-session" , EMPTY_TURNS_FIXTURE ] , cwd ) ;
124- expectCLIFailure ( result ) ;
125- expect ( result . stderr . toLowerCase ( ) ) . toMatch ( / n o u s a b l e t u r n s | n o u s e r o r a s s i s t a n t / ) ;
126- // No file written — empty exports should not produce a content-free source.
127- const files = await readdir ( path . join ( cwd , "sources" ) ) ;
128- expect ( files ) . toEqual ( [ ] ) ;
143+ await expectIngestSessionFailureWithoutWrite (
144+ result ,
145+ / n o u s a b l e t u r n s | n o u s e r o r a s s i s t a n t / ,
146+ cwd ,
147+ ) ;
129148 } ) ;
130149} ) ;
131150
132151describe ( "ingest-session — bulk directory import" , ( ) => {
133152 it ( "mixed good and bad files: good ones import, bad ones warn, exit 0" , async ( ) => {
134- const cwd = await makeWorkspace ( "bulk-mixed" ) ;
153+ const cwd = await makeWorkspace ( ) ;
135154 const dir = path . join ( cwd , "sessions" ) ;
136155 await mkdir ( dir , { recursive : true } ) ;
137156 await copyFile ( CLAUDE_FIXTURE , path . join ( dir , "claude.jsonl" ) ) ;
@@ -148,7 +167,7 @@ describe("ingest-session — bulk directory import", () => {
148167 } ) ;
149168
150169 it ( "directory with only malformed/unrecognised files exits non-zero" , async ( ) => {
151- const cwd = await makeWorkspace ( "bulk-all-bad" ) ;
170+ const cwd = await makeWorkspace ( ) ;
152171 const dir = path . join ( cwd , "sessions" ) ;
153172 await mkdir ( dir , { recursive : true } ) ;
154173 await copyFile ( MALFORMED_FIXTURE , path . join ( dir , "malformed.jsonl" ) ) ;
@@ -162,7 +181,7 @@ describe("ingest-session — bulk directory import", () => {
162181
163182describe ( "ingest-session — filename safety (#35/#36 inheritance)" , ( ) => {
164183 it ( "two sessions with the same title from different files do not silently overwrite" , async ( ) => {
165- const cwd = await makeWorkspace ( "dup-titles" ) ;
184+ const cwd = await makeWorkspace ( ) ;
166185 // Build two distinct claude sessions whose first user turn produces the
167186 // same title — same slug, different sources. The second ingest must
168187 // disambiguate via the hash suffix shared with normal ingest (#36).
@@ -194,7 +213,7 @@ describe("ingest-session — filename safety (#35/#36 inheritance)", () => {
194213 } ) ;
195214
196215 it ( "session whose title slugifies to empty fails loudly without writing a dotfile" , async ( ) => {
197- const cwd = await makeWorkspace ( "empty-slug" ) ;
216+ const cwd = await makeWorkspace ( ) ;
198217 const file = path . join ( cwd , "emoji-only.jsonl" ) ;
199218 // Title derives from the first user turn content. Use pure-emoji content
200219 // so slugify returns "" and the empty-slug guard from #35 fires.
@@ -206,9 +225,10 @@ describe("ingest-session — filename safety (#35/#36 inheritance)", () => {
206225 ) ;
207226
208227 const result = await runCLI ( [ "ingest-session" , file ] , cwd ) ;
209- expectCLIFailure ( result ) ;
210- expect ( result . stderr . toLowerCase ( ) ) . toMatch ( / c o u l d n o t d e r i v e a f i l e n a m e / ) ;
211- const files = await readdir ( path . join ( cwd , "sources" ) ) ;
212- expect ( files ) . not . toContain ( ".md" ) ;
228+ await expectIngestSessionFailureWithoutWrite (
229+ result ,
230+ / c o u l d n o t d e r i v e a f i l e n a m e / ,
231+ cwd ,
232+ ) ;
213233 } ) ;
214234} ) ;
0 commit comments