anvilsign in

collin/strudel-claude

main / node-tui / sessions.test.ts
1// `node --test node-tui/sessions.test.ts`
2//
3// Runs against a temporary directory via STRUDEL_SESSIONS, so it never touches
4// a real library.
5import { test, before, after } from 'node:test';
6import assert from 'node:assert/strict';
7import fs from 'node:fs/promises';
8import os from 'node:os';
9import path from 'node:path';
10
11const DIR = await fs.mkdtemp(path.join(os.tmpdir(), 'strudel-sessions-'));
12process.env['STRUDEL_SESSIONS'] = DIR;
13
14// Imported after the env var is set — SESSIONS_DIR is resolved at module load.
15const sessions = await import('./sessions.ts');
16import type { Turn } from './claude.ts';
17
18after(async () => { await fs.rm(DIR, { recursive: true, force: true }); });
19before(async () => { await fs.mkdir(DIR, { recursive: true }); });
20
21const HISTORY: Turn[] = [
22 { role: 'user', content: 'make me a techno pattern' },
23 { role: 'assistant', content: 'Here you go.', code: 's("bd*4")' },
24];
25
26test('a saved session round-trips code and history together', async () => {
27 await sessions.save('techno', 's("bd*4")', HISTORY);
28 const [meta] = await sessions.list();
29 assert.ok(meta);
30 const loaded = await sessions.load(meta);
31 assert.equal(loaded.code, 's("bd*4")');
32 assert.deepEqual(loaded.history, HISTORY);
33});
34
35test('names are slugified into something a shell will not fight', () => {
36 assert.equal(sessions.slugify('Make Me A Techno!! pattern'), 'make-me-a-techno-pattern');
37 assert.equal(sessions.slugify(' '), 'untitled');
38 assert.equal(sessions.slugify('../../etc/passwd'), 'etc-passwd');
39});
40
41test('a name is derived from the first user message', () => {
42 assert.equal(sessions.nameFrom(HISTORY), 'make me a techno pattern');
43 assert.equal(sessions.nameFrom([]), null);
44 // Assistant-first history has nothing to name itself after.
45 assert.equal(sessions.nameFrom([{ role: 'assistant', content: 'hi' }]), null);
46});
47
48test('a long first message is truncated', () => {
49 const long = 'x'.repeat(100);
50 const name = sessions.nameFrom([{ role: 'user', content: long }]);
51 assert.equal(name?.length, 33); // 32 chars + ellipsis
52 assert.ok(name?.endsWith('…'));
53});
54
55test('.str files are listed as code-only sessions', async () => {
56 await fs.writeFile(path.join(DIR, 'plain.str'), 's("hh*8")\n');
57 const list = await sessions.list();
58 const plain = list.find((s) => s.name === 'plain');
59 assert.ok(plain);
60 assert.equal(plain.hasHistory, false);
61 const loaded = await sessions.load(plain);
62 assert.equal(loaded.code.trim(), 's("hh*8")');
63 assert.deepEqual(loaded.history, []);
64});
65
66test('a JSON session shadows a .str export of the same name', async () => {
67 await sessions.save('both', 's("json")', HISTORY);
68 await sessions.exportPlain('both', 's("plain")');
69 const matches = (await sessions.list()).filter((s) => s.name === 'both');
70 assert.equal(matches.length, 1, 'the same name must not appear twice');
71 assert.equal(matches[0]?.hasHistory, true);
72});
73
74test('a corrupt history costs the transcript, not the pattern', async () => {
75 const file = path.join(DIR, 'broken.json');
76 await fs.writeFile(file, JSON.stringify({ code: 's("bd")', history: 'not an array' }));
77 const loaded = await sessions.load({
78 name: 'broken', file, updatedAt: 0, hasHistory: true,
79 });
80 assert.equal(loaded.code, 's("bd")');
81 assert.deepEqual(loaded.history, []);
82});
83
84test('malformed turns are dropped individually', async () => {
85 const file = path.join(DIR, 'partial.json');
86 await fs.writeFile(file, JSON.stringify({
87 code: 'x',
88 history: [{ role: 'user', content: 'ok' }, null, { role: 'user' }, 'nope'],
89 }));
90 const loaded = await sessions.load({
91 name: 'partial', file, updatedAt: 0, hasHistory: true,
92 });
93 assert.deepEqual(loaded.history, [{ role: 'user', content: 'ok' }]);
94});
95
96test('the most recently touched session is listed first', async () => {
97 await sessions.save('older', 'a', []);
98 await new Promise((r) => setTimeout(r, 10));
99 await sessions.save('newer', 'b', []);
100 const list = await sessions.list();
101 assert.equal(list[0]?.name, 'newer');
102});
103
104test('listing an absent directory is empty, not an error', async () => {
105 const gone = path.join(DIR, 'nope');
106 const loaded = await fs.readdir(gone).catch(() => null);
107 assert.equal(loaded, null); // precondition: really absent
108 // list() reads SESSIONS_DIR, which exists; the guarantee is that a missing
109 // directory yields [] rather than throwing, exercised on first run.
110 assert.ok(Array.isArray(await sessions.list()));
111});