2026-07-08 20:43:54 +08:00
|
|
|
// Regression test for the message write semantics (formerly the indexJsonl
|
|
|
|
|
// INSERT-OR-REPLACE vs ON-CONFLICT drift; now enforced through the shared
|
|
|
|
|
// persist layer). Re-indexing a claude session must upsert messages (stable
|
|
|
|
|
// rowid, no FTS churn) and, because claude parses fresh from an empty cursor
|
|
|
|
|
// (countMode 'total'), must replace message_count rather than accumulate.
|
2026-07-08 17:23:55 +08:00
|
|
|
|
|
|
|
|
import { test } from 'node:test';
|
|
|
|
|
import assert from 'node:assert/strict';
|
|
|
|
|
import { createRequire } from 'node:module';
|
2026-07-08 20:43:54 +08:00
|
|
|
import { mkdtempSync, writeFileSync, readFileSync } from 'node:fs';
|
2026-07-08 17:23:55 +08:00
|
|
|
import { tmpdir } from 'node:os';
|
|
|
|
|
import { join } from 'node:path';
|
|
|
|
|
|
2026-07-12 00:28:17 +08:00
|
|
|
import { parse } from '../packages/core/src/providers/claude.ts';
|
|
|
|
|
import { persist } from '../packages/core/src/persist.ts';
|
2026-07-08 17:23:55 +08:00
|
|
|
|
|
|
|
|
const require = createRequire(import.meta.url);
|
|
|
|
|
const { DatabaseSync } = require('node:sqlite');
|
2026-07-12 00:28:17 +08:00
|
|
|
const SCHEMA = readFileSync(new URL('../packages/core/src/schema.sql', import.meta.url), 'utf8');
|
2026-07-08 17:23:55 +08:00
|
|
|
|
2026-07-08 20:43:54 +08:00
|
|
|
function fixtureUnit() {
|
2026-07-08 17:23:55 +08:00
|
|
|
const dir = mkdtempSync(join(tmpdir(), 'obelisk-drift-'));
|
2026-07-08 20:43:54 +08:00
|
|
|
const jsonlPath = join(dir, 'sess.jsonl');
|
2026-07-08 17:23:55 +08:00
|
|
|
const lines = [
|
|
|
|
|
{ uuid: 'u-1', type: 'user', timestamp: '2026-06-10T10:00:00Z', cwd: '/tmp/proj', message: { role: 'user', content: 'first question' } },
|
|
|
|
|
{ uuid: 'a-1', type: 'assistant', timestamp: '2026-06-10T10:00:05Z', message: { role: 'assistant', model: 'claude-opus', content: 'first answer' } },
|
|
|
|
|
{ uuid: 'u-2', type: 'user', timestamp: '2026-06-10T10:00:10Z', cwd: '/tmp/proj', message: { role: 'user', content: 'second question' } },
|
|
|
|
|
];
|
|
|
|
|
writeFileSync(jsonlPath, lines.map(l => JSON.stringify(l)).join('\n') + '\n');
|
2026-07-08 20:43:54 +08:00
|
|
|
return { key: jsonlPath, sessionId: 'sid-drift', project: 'quiet-zero' };
|
2026-07-08 17:23:55 +08:00
|
|
|
}
|
|
|
|
|
|
2026-07-08 20:43:54 +08:00
|
|
|
test('re-indexing upserts messages (stable rowid) and replaces message_count', () => {
|
2026-07-08 17:23:55 +08:00
|
|
|
const db = new DatabaseSync(':memory:');
|
|
|
|
|
db.exec(SCHEMA);
|
2026-07-08 20:43:54 +08:00
|
|
|
const unit = fixtureUnit();
|
2026-07-08 17:23:55 +08:00
|
|
|
|
2026-07-08 20:43:54 +08:00
|
|
|
persist(db, unit, parse(unit, null));
|
2026-07-08 17:23:55 +08:00
|
|
|
const countAfterFirst = db.prepare('SELECT message_count FROM sessions WHERE id=?').get('sid-drift').message_count;
|
|
|
|
|
const rowidAfterFirst = db.prepare('SELECT rowid FROM messages WHERE uuid=?').get('u-1').rowid;
|
2026-07-08 20:43:54 +08:00
|
|
|
assert.equal(countAfterFirst, 3);
|
|
|
|
|
assert.equal(db.prepare('SELECT COUNT(*) AS c FROM messages').get().c, 3);
|
2026-07-08 17:23:55 +08:00
|
|
|
|
2026-07-08 20:43:54 +08:00
|
|
|
// Re-index the same session from scratch (fresh parse → countMode 'total').
|
|
|
|
|
persist(db, unit, parse(unit, null));
|
2026-07-08 17:23:55 +08:00
|
|
|
|
|
|
|
|
const countAfterSecond = db.prepare('SELECT message_count FROM sessions WHERE id=?').get('sid-drift').message_count;
|
|
|
|
|
const rowidAfterSecond = db.prepare('SELECT rowid FROM messages WHERE uuid=?').get('u-1').rowid;
|
2026-07-08 20:43:54 +08:00
|
|
|
assert.equal(countAfterSecond, 3, 'message_count is replaced, not accumulated (would be 6 under the old bug)');
|
|
|
|
|
assert.equal(db.prepare('SELECT COUNT(*) AS c FROM messages').get().c, 3, 'no duplicate rows');
|
|
|
|
|
assert.equal(rowidAfterSecond, rowidAfterFirst, 'upsert preserves rowid (INSERT OR REPLACE would churn it)');
|
2026-07-08 17:23:55 +08:00
|
|
|
|
|
|
|
|
db.close();
|
|
|
|
|
});
|