2026-07-20 22:43:23 +08:00
|
|
|
import { test } from 'node:test';
|
|
|
|
|
import assert from 'node:assert/strict';
|
|
|
|
|
import { createRequire } from 'node:module';
|
|
|
|
|
import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs';
|
|
|
|
|
import { tmpdir } from 'node:os';
|
|
|
|
|
import { join } from 'node:path';
|
|
|
|
|
|
|
|
|
|
import { buildIndex } from '../app/src/main/indexer.ts';
|
2026-07-21 00:58:34 +08:00
|
|
|
import { createKimiProvider } from '../packages/core/src/providers/kimi.ts';
|
2026-07-20 22:43:23 +08:00
|
|
|
|
|
|
|
|
const require = createRequire(import.meta.url);
|
|
|
|
|
const { DatabaseSync } = require('node:sqlite');
|
|
|
|
|
|
|
|
|
|
class TestDatabase {
|
|
|
|
|
constructor(dbPath) {
|
|
|
|
|
this.db = new DatabaseSync(dbPath);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pragma(statement) { this.db.exec(`PRAGMA ${statement}`); }
|
|
|
|
|
exec(sql) { return this.db.exec(sql); }
|
|
|
|
|
prepare(sql) { return this.db.prepare(sql); }
|
|
|
|
|
close() { return this.db.close(); }
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-21 00:58:34 +08:00
|
|
|
function writeSession(kimiDir, { userSlash = false } = {}) {
|
2026-07-20 22:43:23 +08:00
|
|
|
const sessionDir = join(kimiDir, 'sessions', 'workspace-1', 'session-index-1');
|
|
|
|
|
const mainDir = join(sessionDir, 'agents', 'main');
|
|
|
|
|
mkdirSync(mainDir, { recursive: true });
|
|
|
|
|
writeFileSync(join(sessionDir, 'state.json'), JSON.stringify({
|
|
|
|
|
title: 'Indexed Kimi session',
|
|
|
|
|
workDir: '/tmp/indexed-kimi',
|
|
|
|
|
createdAt: '2026-07-20T10:00:00.000Z',
|
|
|
|
|
updatedAt: '2026-07-20T10:01:00.000Z',
|
|
|
|
|
agents: { main: { type: 'main' } },
|
|
|
|
|
}));
|
|
|
|
|
const wirePath = join(mainDir, 'wire.jsonl');
|
|
|
|
|
const records = [
|
|
|
|
|
{ type: 'metadata', protocol_version: '1.5', created_at: 1753005600000 },
|
2026-07-21 00:58:34 +08:00
|
|
|
{ type: 'context.append_message', time: 1753005601000, message: userSlash
|
|
|
|
|
? {
|
|
|
|
|
role: 'user', content: 'Expanded skill instructions.', toolCalls: [],
|
|
|
|
|
origin: {
|
|
|
|
|
kind: 'skill_activation', trigger: 'user-slash', skillName: 'obelisk',
|
|
|
|
|
skillArgs: 'find prior decisions',
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
: { role: 'user', content: [{ type: 'text', text: 'kimi index needle' }], toolCalls: [], origin: { kind: 'user' } } },
|
2026-07-20 22:43:23 +08:00
|
|
|
];
|
|
|
|
|
writeFileSync(wirePath, records.map((record) => JSON.stringify(record)).join('\n') + '\n');
|
|
|
|
|
return { sessionDir, wirePath, records };
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-03 00:49:37 +08:00
|
|
|
function writePlaceholderSession(kimiDir, { userPrompt = false } = {}) {
|
|
|
|
|
const sessionDir = join(kimiDir, 'sessions', 'workspace-1', 'session-placeholder-1');
|
|
|
|
|
const mainDir = join(sessionDir, 'agents', 'main');
|
|
|
|
|
mkdirSync(mainDir, { recursive: true });
|
|
|
|
|
writeFileSync(join(sessionDir, 'state.json'), JSON.stringify({
|
|
|
|
|
title: 'New Session',
|
|
|
|
|
workDir: '/tmp/indexed-kimi',
|
|
|
|
|
createdAt: '2026-07-20T10:00:00.000Z',
|
|
|
|
|
updatedAt: '2026-07-20T10:00:00.000Z',
|
|
|
|
|
agents: { main: { type: 'main' } },
|
|
|
|
|
}));
|
|
|
|
|
const records = [
|
|
|
|
|
{ type: 'metadata', protocol_version: '1.5', created_at: 1753005600000 },
|
|
|
|
|
{ type: 'config.update', profileName: 'agent', systemPrompt: 'Kimi Code CLI' },
|
|
|
|
|
{ type: 'tools.set_active_tools', tools: [] },
|
|
|
|
|
{ type: 'config.update', modelAlias: 'kimi-code/kimi-for-coding' },
|
|
|
|
|
];
|
|
|
|
|
if (userPrompt) {
|
|
|
|
|
records.push({
|
|
|
|
|
type: 'context.append_message',
|
|
|
|
|
time: 1753005601000,
|
|
|
|
|
message: {
|
|
|
|
|
role: 'user',
|
|
|
|
|
content: [{ type: 'text', text: 'real prompt before metadata catches up' }],
|
|
|
|
|
toolCalls: [],
|
|
|
|
|
origin: { kind: 'user' },
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
const wirePath = join(mainDir, 'wire.jsonl');
|
|
|
|
|
writeFileSync(
|
|
|
|
|
wirePath,
|
|
|
|
|
records.map((record) => JSON.stringify(record)).join('\n') + '\n',
|
|
|
|
|
);
|
|
|
|
|
return { sessionDir, wirePath };
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-20 22:43:23 +08:00
|
|
|
test('app build indexes Kimi sessions through the provider registry without changing schema', () => {
|
|
|
|
|
const home = mkdtempSync(join(tmpdir(), 'obelisk-kimi-index-'));
|
|
|
|
|
const claudeDir = join(home, '.claude');
|
|
|
|
|
const codexDir = join(home, '.codex');
|
|
|
|
|
const kimiDir = join(home, '.kimi-code');
|
|
|
|
|
const dbPath = join(home, '.obelisk', 'obelisk.sqlite');
|
|
|
|
|
writeSession(kimiDir);
|
|
|
|
|
|
|
|
|
|
const first = buildIndex({
|
|
|
|
|
claudeDir,
|
|
|
|
|
codexDir,
|
|
|
|
|
providerRoots: { kimi: kimiDir },
|
|
|
|
|
dbPath,
|
|
|
|
|
DatabaseImpl: TestDatabase,
|
|
|
|
|
});
|
|
|
|
|
assert.deepEqual(first.affectedSessionIds, ['kimi:session-index-1']);
|
|
|
|
|
|
|
|
|
|
const db = new TestDatabase(dbPath);
|
|
|
|
|
assert.deepEqual(
|
|
|
|
|
db.prepare('SELECT id,title,source,message_count FROM sessions').all().map((row) => ({ ...row })),
|
|
|
|
|
[{ id: 'kimi:session-index-1', title: 'Indexed Kimi session', source: 'kimi', message_count: 1 }],
|
|
|
|
|
);
|
|
|
|
|
assert.equal(db.prepare('SELECT text FROM messages').get().text, 'kimi index needle');
|
|
|
|
|
assert.equal(db.prepare('SELECT COUNT(*) AS c FROM index_state WHERE jsonl_path = ?').get(
|
|
|
|
|
join(kimiDir, 'sessions', 'workspace-1', 'session-index-1'),
|
|
|
|
|
).c, 1);
|
|
|
|
|
const schema = db.prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='messages'").get().sql;
|
|
|
|
|
assert.doesNotMatch(schema, /kimi/i);
|
|
|
|
|
db.close();
|
|
|
|
|
|
|
|
|
|
const second = buildIndex({
|
|
|
|
|
claudeDir,
|
|
|
|
|
codexDir,
|
|
|
|
|
providerRoots: { kimi: kimiDir },
|
|
|
|
|
dbPath,
|
|
|
|
|
DatabaseImpl: TestDatabase,
|
|
|
|
|
});
|
|
|
|
|
assert.deepEqual(second.affectedSessionIds, []);
|
|
|
|
|
});
|
|
|
|
|
|
2026-08-03 00:49:37 +08:00
|
|
|
test('app build excludes never-started Kimi placeholder sessions', () => {
|
|
|
|
|
const home = mkdtempSync(join(tmpdir(), 'obelisk-kimi-placeholder-'));
|
|
|
|
|
const kimiDir = join(home, '.kimi-code');
|
|
|
|
|
const dbPath = join(home, '.obelisk', 'obelisk.sqlite');
|
|
|
|
|
writePlaceholderSession(kimiDir);
|
|
|
|
|
|
|
|
|
|
buildIndex({
|
|
|
|
|
claudeDir: join(home, '.claude'),
|
|
|
|
|
codexDir: join(home, '.codex'),
|
|
|
|
|
providerRoots: { kimi: kimiDir },
|
|
|
|
|
dbPath,
|
|
|
|
|
DatabaseImpl: TestDatabase,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const db = new TestDatabase(dbPath);
|
|
|
|
|
assert.deepEqual(
|
|
|
|
|
db.prepare("SELECT id,title,message_count FROM sessions WHERE source='kimi'").all().map((row) => ({ ...row })),
|
|
|
|
|
[],
|
|
|
|
|
);
|
|
|
|
|
db.close();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('app build keeps a prompted Kimi session while placeholder metadata catches up', () => {
|
|
|
|
|
const home = mkdtempSync(join(tmpdir(), 'obelisk-kimi-prompted-placeholder-'));
|
|
|
|
|
const kimiDir = join(home, '.kimi-code');
|
|
|
|
|
const dbPath = join(home, '.obelisk', 'obelisk.sqlite');
|
|
|
|
|
writePlaceholderSession(kimiDir, { userPrompt: true });
|
|
|
|
|
|
|
|
|
|
buildIndex({
|
|
|
|
|
claudeDir: join(home, '.claude'),
|
|
|
|
|
codexDir: join(home, '.codex'),
|
|
|
|
|
providerRoots: { kimi: kimiDir },
|
|
|
|
|
dbPath,
|
|
|
|
|
DatabaseImpl: TestDatabase,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const db = new TestDatabase(dbPath);
|
|
|
|
|
assert.deepEqual(
|
|
|
|
|
db.prepare("SELECT id,title,message_count FROM sessions WHERE source='kimi'").all().map((row) => ({ ...row })),
|
|
|
|
|
[{ id: 'kimi:session-placeholder-1', title: 'New Session', message_count: 1 }],
|
|
|
|
|
);
|
|
|
|
|
db.close();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('Kimi marker upgrade retracts previously indexed placeholder sessions', () => {
|
|
|
|
|
const home = mkdtempSync(join(tmpdir(), 'obelisk-kimi-placeholder-replay-'));
|
|
|
|
|
const kimiDir = join(home, '.kimi-code');
|
|
|
|
|
const dbPath = join(home, '.obelisk', 'obelisk.sqlite');
|
|
|
|
|
const { wirePath } = writePlaceholderSession(kimiDir);
|
|
|
|
|
const options = {
|
|
|
|
|
claudeDir: join(home, '.claude'),
|
|
|
|
|
codexDir: join(home, '.codex'),
|
|
|
|
|
providerRoots: { kimi: kimiDir },
|
|
|
|
|
dbPath,
|
|
|
|
|
DatabaseImpl: TestDatabase,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
buildIndex(options);
|
|
|
|
|
let db = new TestDatabase(dbPath);
|
|
|
|
|
const currentMarker = createKimiProvider({ rootDir: kimiDir }).indexVersionMarker;
|
|
|
|
|
db.prepare('DELETE FROM index_state WHERE jsonl_path=?').run(currentMarker);
|
|
|
|
|
db.prepare(
|
|
|
|
|
'INSERT INTO index_state (jsonl_path,mtime,lines_processed) VALUES (?,0,0)',
|
|
|
|
|
).run('__kimi_canonical_transcript_v3__');
|
|
|
|
|
db.prepare(`
|
|
|
|
|
INSERT INTO sessions (id,title,message_count,jsonl_path,source)
|
|
|
|
|
VALUES (?,?,?,?,?)
|
|
|
|
|
`).run('kimi:session-placeholder-1', 'New Session', 0, wirePath, 'kimi');
|
|
|
|
|
db.close();
|
|
|
|
|
|
|
|
|
|
buildIndex(options);
|
|
|
|
|
db = new TestDatabase(dbPath);
|
|
|
|
|
assert.deepEqual(
|
|
|
|
|
db.prepare("SELECT id,title,message_count FROM sessions WHERE source='kimi'").all().map((row) => ({ ...row })),
|
|
|
|
|
[],
|
|
|
|
|
);
|
|
|
|
|
db.close();
|
|
|
|
|
});
|
|
|
|
|
|
2026-07-20 22:43:23 +08:00
|
|
|
test('Kimi undo and clear replace the indexed session instead of leaving stale rows', () => {
|
|
|
|
|
const home = mkdtempSync(join(tmpdir(), 'obelisk-kimi-replay-'));
|
|
|
|
|
const claudeDir = join(home, '.claude');
|
|
|
|
|
const codexDir = join(home, '.codex');
|
|
|
|
|
const kimiDir = join(home, '.kimi-code');
|
|
|
|
|
const dbPath = join(home, '.obelisk', 'obelisk.sqlite');
|
|
|
|
|
const { wirePath, records } = writeSession(kimiDir);
|
|
|
|
|
const assistant = {
|
|
|
|
|
type: 'context.append_loop_event',
|
|
|
|
|
time: 1753005602000,
|
|
|
|
|
event: { type: 'content.part', uuid: 'answer-1', stepUuid: 'step-1', part: { type: 'text', text: 'answer removed by undo' } },
|
|
|
|
|
};
|
|
|
|
|
writeFileSync(wirePath, [...records, assistant].map(record => JSON.stringify(record)).join('\n') + '\n');
|
|
|
|
|
|
|
|
|
|
const options = {
|
|
|
|
|
claudeDir,
|
|
|
|
|
codexDir,
|
|
|
|
|
providerRoots: { kimi: kimiDir },
|
|
|
|
|
dbPath,
|
|
|
|
|
DatabaseImpl: TestDatabase,
|
|
|
|
|
};
|
|
|
|
|
buildIndex(options);
|
|
|
|
|
let db = new TestDatabase(dbPath);
|
|
|
|
|
assert.equal(db.prepare('SELECT COUNT(*) AS c FROM messages').get().c, 2);
|
|
|
|
|
db.close();
|
|
|
|
|
|
|
|
|
|
// Kimi undo shrinks the durable wire transcript.
|
|
|
|
|
writeFileSync(wirePath, records.map(record => JSON.stringify(record)).join('\n') + '\n');
|
|
|
|
|
buildIndex(options);
|
|
|
|
|
db = new TestDatabase(dbPath);
|
|
|
|
|
assert.equal(db.prepare('SELECT COUNT(*) AS c FROM messages').get().c, 1);
|
|
|
|
|
assert.equal(db.prepare("SELECT COUNT(*) AS c FROM messages WHERE text LIKE '%removed by undo%'").get().c, 0);
|
|
|
|
|
db.close();
|
|
|
|
|
|
|
|
|
|
// Clear retains the session container but removes all projected messages.
|
|
|
|
|
writeFileSync(wirePath, JSON.stringify(records[0]) + '\n');
|
|
|
|
|
buildIndex(options);
|
|
|
|
|
db = new TestDatabase(dbPath);
|
|
|
|
|
assert.equal(db.prepare('SELECT COUNT(*) AS c FROM messages').get().c, 0);
|
|
|
|
|
assert.equal(db.prepare('SELECT message_count FROM sessions WHERE id=?').get('kimi:session-index-1').message_count, 0);
|
|
|
|
|
db.close();
|
|
|
|
|
});
|
2026-07-21 00:58:34 +08:00
|
|
|
|
2026-07-21 19:29:14 +08:00
|
|
|
test('Kimi canonical transcript marker replays unchanged sessions once', () => {
|
2026-07-21 00:58:34 +08:00
|
|
|
const home = mkdtempSync(join(tmpdir(), 'obelisk-kimi-prompt-marker-'));
|
|
|
|
|
const claudeDir = join(home, '.claude');
|
|
|
|
|
const codexDir = join(home, '.codex');
|
|
|
|
|
const kimiDir = join(home, '.kimi-code');
|
|
|
|
|
const dbPath = join(home, '.obelisk', 'obelisk.sqlite');
|
|
|
|
|
writeSession(kimiDir, { userSlash: true });
|
|
|
|
|
const options = {
|
|
|
|
|
claudeDir,
|
|
|
|
|
codexDir,
|
|
|
|
|
providerRoots: { kimi: kimiDir },
|
|
|
|
|
dbPath,
|
|
|
|
|
DatabaseImpl: TestDatabase,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
buildIndex(options);
|
|
|
|
|
let db = new TestDatabase(dbPath);
|
|
|
|
|
const marker = createKimiProvider({ rootDir: kimiDir }).indexVersionMarker;
|
2026-08-03 00:49:37 +08:00
|
|
|
assert.equal(marker, '__kimi_canonical_transcript_v4__');
|
2026-07-21 00:58:34 +08:00
|
|
|
db.prepare("UPDATE messages SET text='stale expanded instructions', is_meta=1 WHERE source='kimi'").run();
|
|
|
|
|
db.prepare('DELETE FROM index_state WHERE jsonl_path=?').run(marker);
|
|
|
|
|
db.close();
|
|
|
|
|
|
|
|
|
|
const replay = buildIndex(options);
|
|
|
|
|
assert.deepEqual(replay.affectedSessionIds, ['kimi:session-index-1']);
|
|
|
|
|
db = new TestDatabase(dbPath);
|
|
|
|
|
assert.deepEqual(
|
|
|
|
|
{ ...db.prepare("SELECT text,is_meta FROM messages WHERE source='kimi'").get() },
|
|
|
|
|
{ text: '/obelisk find prior decisions', is_meta: 0 },
|
|
|
|
|
);
|
|
|
|
|
assert.equal(db.prepare('SELECT COUNT(*) AS c FROM index_state WHERE jsonl_path=?').get(marker).c, 1);
|
|
|
|
|
db.close();
|
|
|
|
|
});
|