mirror of
https://github.com/agoramachina/claude-exporter.git
synced 2026-07-10 00:12:08 +00:00
Add vitest harness for utils.js with 17 starter tests
This commit is contained in:
parent
56a18c99b6
commit
950254598b
8 changed files with 1964 additions and 7 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
node_modules/
|
||||
|
|
@ -91,6 +91,14 @@ Any export producing more than one file should always create a ZIP — never tri
|
|||
|
||||
When merging `testing` → `main` for a release, flip both manifest names to drop "Beta" as part of the merge.
|
||||
|
||||
## Testing
|
||||
|
||||
- **Vitest** lives in `src/`. Run tests with `npm test` (one-shot) or `npm run test:watch` (watch mode) from `src/`.
|
||||
- Test files live in `src/tests/` and import from `src/chrome/utils.js` (the canonical copy).
|
||||
- `firefox/utils.js` is a mirror — if it drifts from `chrome/utils.js`, the tests won't catch it. Keep them in sync per the existing rule.
|
||||
- `utils.js` has a conditional `module.exports` block at the bottom that fires only when `module` is defined (Node/vitest). Browser extensions ignore it because the global is undefined.
|
||||
- `node_modules/` and `package-lock.json` live under `src/` and are gitignored / not part of the release ZIPs.
|
||||
|
||||
## Architecture Notes
|
||||
|
||||
### Content Script Injection
|
||||
|
|
|
|||
22
TODO.md
22
TODO.md
|
|
@ -118,6 +118,12 @@
|
|||
- Click the org ID row in the browse settings dropdown to copy it
|
||||
- Toast confirms "Org ID copied to clipboard"
|
||||
|
||||
- **Vitest unit tests for `utils.js`** (post-v1.9.1)
|
||||
- 17 starter tests covering the recently-fixed bugs and core export logic
|
||||
- Regression coverage for `tool_use.name === 'artifacts'` filter, branch traversal, file extension mapping
|
||||
- `npm test` from `src/`; tests live in `src/tests/`
|
||||
- Canonical source is `chrome/utils.js`; `firefox/utils.js` mirror must stay in sync
|
||||
|
||||
## Pending 🔄
|
||||
|
||||
### Critical Priority 🔴
|
||||
|
|
@ -202,10 +208,11 @@
|
|||
- Filter by project, model, artifact
|
||||
|
||||
- **Advanced settings menu**
|
||||
- *Language settings*
|
||||
- *Custom CSS*
|
||||
- *Regex mode*
|
||||
- *Custom date/time format*
|
||||
- Verbosity toggle & Debug log
|
||||
- Language settings
|
||||
- Custom CSS
|
||||
- Regex mode
|
||||
- Custom date/time format
|
||||
- Custom format string (e.g. `%d/%m/%Y %H:%M`)
|
||||
- Toggle time display on/off
|
||||
|
||||
|
|
@ -233,7 +240,7 @@
|
|||
- Option to use regex patterns in the search bar
|
||||
- Toggle between plain text and regex mode
|
||||
|
||||
- **Help / tutorial in settings menu** (low priority)
|
||||
- **Help / tutorial in settings menu**
|
||||
- Add a help/getting started option to the settings dropdown
|
||||
- Quick overview of features, export options, keyboard shortcuts
|
||||
|
||||
|
|
@ -241,6 +248,11 @@
|
|||
- Export progress spinner
|
||||
- Test connection spinner
|
||||
|
||||
- **Update screenshots**
|
||||
- Include browse page and popup
|
||||
- Include dark and light mode
|
||||
- 1280x800 or 640x400 jpeg or 24-bit png (no alpha)
|
||||
|
||||
## Bugs 🐛
|
||||
|
||||
(none currently open)
|
||||
|
|
@ -581,4 +581,19 @@ function extractArtifactFiles(data, artifactFormat = 'original') {
|
|||
return artifactFiles;
|
||||
}
|
||||
// Functions are available globally in the browser context
|
||||
// No need for module.exports in browser extensions
|
||||
// In Node (vitest), expose them via module.exports for testing
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = {
|
||||
getCurrentBranch,
|
||||
convertToMarkdown,
|
||||
convertToText,
|
||||
downloadFile,
|
||||
extractArtifactsFromMessage,
|
||||
extractArtifactsFromText,
|
||||
extractArtifacts,
|
||||
getFileExtension,
|
||||
isProgrammingLanguage,
|
||||
convertArtifactFormat,
|
||||
extractArtifactFiles,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -581,4 +581,19 @@ function extractArtifactFiles(data, artifactFormat = 'original') {
|
|||
return artifactFiles;
|
||||
}
|
||||
// Functions are available globally in the browser context
|
||||
// No need for module.exports in browser extensions
|
||||
// In Node (vitest), expose them via module.exports for testing
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = {
|
||||
getCurrentBranch,
|
||||
convertToMarkdown,
|
||||
convertToText,
|
||||
downloadFile,
|
||||
extractArtifactsFromMessage,
|
||||
extractArtifactsFromText,
|
||||
extractArtifacts,
|
||||
getFileExtension,
|
||||
isProgrammingLanguage,
|
||||
convertArtifactFormat,
|
||||
extractArtifactFiles,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
1583
package-lock.json
generated
Normal file
1583
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
12
package.json
Normal file
12
package.json
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"name": "claude-exporter-tests",
|
||||
"private": true,
|
||||
"description": "Test harness for Claude Exporter (chrome/utils.js is canonical; firefox/utils.js must mirror).",
|
||||
"scripts": {
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vitest": "^3.2.4"
|
||||
}
|
||||
}
|
||||
311
tests/utils.test.js
Normal file
311
tests/utils.test.js
Normal file
|
|
@ -0,0 +1,311 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import utils from '../chrome/utils.js';
|
||||
|
||||
const {
|
||||
getCurrentBranch,
|
||||
convertToMarkdown,
|
||||
extractArtifactsFromMessage,
|
||||
extractArtifactFiles,
|
||||
getFileExtension,
|
||||
isProgrammingLanguage,
|
||||
} = utils;
|
||||
|
||||
// Regression coverage for the bug fixed in v1.9.1: bash/web_search/repl
|
||||
// tool_use entries used to slip through as fake artifacts. Now gated on
|
||||
// `tool_use.name === 'artifacts'`.
|
||||
describe('extractArtifactsFromMessage — tool name filter', () => {
|
||||
it('rejects a bash tool_use even with code_block display content', () => {
|
||||
const message = {
|
||||
content: [
|
||||
{
|
||||
type: 'tool_use',
|
||||
name: 'bash',
|
||||
display_content: {
|
||||
type: 'code_block',
|
||||
code: 'ls -la',
|
||||
language: 'bash',
|
||||
filename: 'cmd.sh',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(extractArtifactsFromMessage(message)).toEqual([]);
|
||||
});
|
||||
|
||||
it('rejects a web_search tool_use', () => {
|
||||
const message = {
|
||||
content: [
|
||||
{
|
||||
type: 'tool_use',
|
||||
name: 'web_search',
|
||||
display_content: {
|
||||
type: 'code_block',
|
||||
code: 'results...',
|
||||
language: 'json',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(extractArtifactsFromMessage(message)).toEqual([]);
|
||||
});
|
||||
|
||||
it('extracts an artifacts tool_use with code_block format', () => {
|
||||
const message = {
|
||||
content: [
|
||||
{
|
||||
type: 'tool_use',
|
||||
name: 'artifacts',
|
||||
display_content: {
|
||||
type: 'code_block',
|
||||
code: 'def hello():\n pass',
|
||||
language: 'python',
|
||||
filename: 'hello.py',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
const artifacts = extractArtifactsFromMessage(message);
|
||||
expect(artifacts).toHaveLength(1);
|
||||
expect(artifacts[0].title).toBe('hello');
|
||||
expect(artifacts[0].language).toBe('python');
|
||||
expect(artifacts[0].content).toBe('def hello():\n pass');
|
||||
});
|
||||
|
||||
it('extracts an artifacts tool_use with json_block format when filename is present', () => {
|
||||
const message = {
|
||||
content: [
|
||||
{
|
||||
type: 'tool_use',
|
||||
name: 'artifacts',
|
||||
display_content: {
|
||||
type: 'json_block',
|
||||
json_block: JSON.stringify({
|
||||
filename: 'app.js',
|
||||
language: 'javascript',
|
||||
code: 'console.log("hi");',
|
||||
}),
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
const artifacts = extractArtifactsFromMessage(message);
|
||||
expect(artifacts).toHaveLength(1);
|
||||
expect(artifacts[0].title).toBe('app');
|
||||
});
|
||||
|
||||
it('rejects a json_block artifacts entry that has no filename', () => {
|
||||
const message = {
|
||||
content: [
|
||||
{
|
||||
type: 'tool_use',
|
||||
name: 'artifacts',
|
||||
display_content: {
|
||||
type: 'json_block',
|
||||
json_block: JSON.stringify({
|
||||
code: 'echo hi',
|
||||
}),
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(extractArtifactsFromMessage(message)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('extractArtifactFiles — end-to-end', () => {
|
||||
function makeConversationWithMessages(messages) {
|
||||
const last = messages[messages.length - 1];
|
||||
return {
|
||||
current_leaf_message_uuid: last.uuid,
|
||||
chat_messages: messages,
|
||||
};
|
||||
}
|
||||
|
||||
it('returns artifact files only from real artifact tool calls', () => {
|
||||
const data = makeConversationWithMessages([
|
||||
{
|
||||
uuid: 'm1',
|
||||
sender: 'human',
|
||||
content: [{ type: 'text', text: 'make me something' }],
|
||||
parent_message_uuid: '00000000-0000-0000-0000-000000000000',
|
||||
},
|
||||
{
|
||||
uuid: 'm2',
|
||||
sender: 'assistant',
|
||||
content: [
|
||||
{
|
||||
type: 'tool_use',
|
||||
name: 'artifacts',
|
||||
display_content: {
|
||||
type: 'code_block',
|
||||
code: '<h1>hi</h1>',
|
||||
language: 'html',
|
||||
filename: 'page.html',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'tool_use',
|
||||
name: 'bash',
|
||||
display_content: {
|
||||
type: 'code_block',
|
||||
code: 'ls',
|
||||
language: 'bash',
|
||||
filename: 'noise.sh',
|
||||
},
|
||||
},
|
||||
],
|
||||
parent_message_uuid: 'm1',
|
||||
},
|
||||
]);
|
||||
const files = extractArtifactFiles(data);
|
||||
expect(files).toHaveLength(1);
|
||||
expect(files[0].filename).toMatch(/\.html$/);
|
||||
});
|
||||
|
||||
it('deduplicates duplicate filenames with a counter suffix', () => {
|
||||
const data = makeConversationWithMessages([
|
||||
{
|
||||
uuid: 'm1',
|
||||
sender: 'assistant',
|
||||
content: [
|
||||
{
|
||||
type: 'tool_use',
|
||||
name: 'artifacts',
|
||||
display_content: {
|
||||
type: 'code_block',
|
||||
code: 'a',
|
||||
language: 'javascript',
|
||||
filename: 'app.js',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'tool_use',
|
||||
name: 'artifacts',
|
||||
display_content: {
|
||||
type: 'code_block',
|
||||
code: 'b',
|
||||
language: 'javascript',
|
||||
filename: 'app.js',
|
||||
},
|
||||
},
|
||||
],
|
||||
parent_message_uuid: '00000000-0000-0000-0000-000000000000',
|
||||
},
|
||||
]);
|
||||
const files = extractArtifactFiles(data);
|
||||
expect(files).toHaveLength(2);
|
||||
const names = files.map(f => f.filename);
|
||||
expect(new Set(names).size).toBe(2); // both unique
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCurrentBranch', () => {
|
||||
it('returns empty array when there are no messages', () => {
|
||||
expect(getCurrentBranch({ chat_messages: [], current_leaf_message_uuid: 'x' })).toEqual([]);
|
||||
});
|
||||
|
||||
it('returns empty array when leaf uuid is missing', () => {
|
||||
expect(getCurrentBranch({ chat_messages: [{ uuid: 'a' }] })).toEqual([]);
|
||||
});
|
||||
|
||||
it('walks from leaf back to root in chronological order', () => {
|
||||
const data = {
|
||||
current_leaf_message_uuid: 'm3',
|
||||
chat_messages: [
|
||||
{ uuid: 'm1', parent_message_uuid: 'root', text: 'first' },
|
||||
{ uuid: 'm2', parent_message_uuid: 'm1', text: 'second' },
|
||||
{ uuid: 'm3', parent_message_uuid: 'm2', text: 'third' },
|
||||
],
|
||||
};
|
||||
const branch = getCurrentBranch(data);
|
||||
expect(branch.map(m => m.uuid)).toEqual(['m1', 'm2', 'm3']);
|
||||
});
|
||||
|
||||
it('only includes messages on the current branch (ignores siblings)', () => {
|
||||
// m1 → m2a → m3 (current leaf), m1 → m2b is a sibling branch and should be excluded
|
||||
const data = {
|
||||
current_leaf_message_uuid: 'm3',
|
||||
chat_messages: [
|
||||
{ uuid: 'm1', parent_message_uuid: 'root', text: 'first' },
|
||||
{ uuid: 'm2a', parent_message_uuid: 'm1', text: 'kept' },
|
||||
{ uuid: 'm2b', parent_message_uuid: 'm1', text: 'sibling' },
|
||||
{ uuid: 'm3', parent_message_uuid: 'm2a', text: 'leaf' },
|
||||
],
|
||||
};
|
||||
const branch = getCurrentBranch(data);
|
||||
expect(branch.map(m => m.uuid)).toEqual(['m1', 'm2a', 'm3']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getFileExtension', () => {
|
||||
it('maps common programming languages correctly', () => {
|
||||
expect(getFileExtension('javascript')).toBe('.js');
|
||||
expect(getFileExtension('python')).toBe('.py');
|
||||
expect(getFileExtension('bash')).toBe('.sh');
|
||||
});
|
||||
|
||||
it('falls back to .txt for unknown languages', () => {
|
||||
expect(getFileExtension('totally-not-a-language')).toBe('.txt');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isProgrammingLanguage', () => {
|
||||
it('recognizes common programming languages', () => {
|
||||
expect(isProgrammingLanguage('javascript')).toBe(true);
|
||||
expect(isProgrammingLanguage('python')).toBe(true);
|
||||
expect(isProgrammingLanguage('rust')).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects markup/document formats', () => {
|
||||
expect(isProgrammingLanguage('markdown')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('convertToMarkdown — smoke test', () => {
|
||||
it('renders both human and assistant message text', () => {
|
||||
const data = {
|
||||
name: 'Test Chat',
|
||||
model: 'claude-sonnet-4-5-20250929',
|
||||
created_at: '2026-04-01T12:00:00Z',
|
||||
updated_at: '2026-04-01T12:00:00Z',
|
||||
current_leaf_message_uuid: 'm2',
|
||||
chat_messages: [
|
||||
{
|
||||
uuid: 'm1',
|
||||
sender: 'human',
|
||||
content: [{ type: 'text', text: 'Hello there' }],
|
||||
parent_message_uuid: '00000000-0000-0000-0000-000000000000',
|
||||
},
|
||||
{
|
||||
uuid: 'm2',
|
||||
sender: 'assistant',
|
||||
content: [{ type: 'text', text: 'General Kenobi' }],
|
||||
parent_message_uuid: 'm1',
|
||||
},
|
||||
],
|
||||
};
|
||||
const md = convertToMarkdown(data, false);
|
||||
expect(md).toContain('Hello there');
|
||||
expect(md).toContain('General Kenobi');
|
||||
});
|
||||
|
||||
it('includes metadata block when includeMetadata is true', () => {
|
||||
const data = {
|
||||
name: 'My Chat',
|
||||
model: 'claude-opus-4-5-20251101',
|
||||
created_at: '2026-04-01T12:00:00Z',
|
||||
updated_at: '2026-04-01T12:00:00Z',
|
||||
current_leaf_message_uuid: 'm1',
|
||||
chat_messages: [
|
||||
{
|
||||
uuid: 'm1',
|
||||
sender: 'human',
|
||||
content: [{ type: 'text', text: 'hi' }],
|
||||
parent_message_uuid: '00000000-0000-0000-0000-000000000000',
|
||||
},
|
||||
],
|
||||
};
|
||||
const md = convertToMarkdown(data, true);
|
||||
expect(md).toContain('My Chat');
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue