throughline 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,155 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { DatabaseSync } from 'node:sqlite';
4
+ import {
5
+ L2_WINDOW,
6
+ countDistinctBodyTurns,
7
+ pickOldestUnsummarizedTurn,
8
+ } from './turn-processor.mjs';
9
+
10
+ function makeDb() {
11
+ const db = new DatabaseSync(':memory:');
12
+ db.exec(`
13
+ CREATE TABLE skeletons (
14
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
15
+ session_id TEXT NOT NULL,
16
+ origin_session_id TEXT,
17
+ turn_number INTEGER NOT NULL,
18
+ role TEXT NOT NULL,
19
+ summary TEXT NOT NULL,
20
+ created_at INTEGER NOT NULL
21
+ );
22
+ CREATE TABLE bodies (
23
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
24
+ session_id TEXT NOT NULL,
25
+ origin_session_id TEXT NOT NULL,
26
+ turn_number INTEGER NOT NULL,
27
+ role TEXT NOT NULL,
28
+ text TEXT NOT NULL,
29
+ token_count INTEGER,
30
+ created_at INTEGER NOT NULL
31
+ );
32
+ `);
33
+ return db;
34
+ }
35
+
36
+ /** 1 往復 (user+assistant) を同じ turn_number で保存。実装と同じペアリング規約。 */
37
+ function insertTurn(db, { session, origin, turn, createdAt }) {
38
+ const stmt = db.prepare(
39
+ `INSERT INTO bodies (session_id, origin_session_id, turn_number, role, text, token_count, created_at)
40
+ VALUES (?, ?, ?, ?, ?, ?, ?)`,
41
+ );
42
+ stmt.run(session, origin, turn, 'user', `u${turn}`, 1, createdAt);
43
+ stmt.run(session, origin, turn, 'assistant', `a${turn}`, 1, createdAt);
44
+ }
45
+
46
+ function insertSkeleton(db, { session, origin, turn, createdAt }) {
47
+ db.prepare(
48
+ `INSERT INTO skeletons (session_id, origin_session_id, turn_number, role, summary, created_at)
49
+ VALUES (?, ?, ?, 'assistant', ?, ?)`,
50
+ ).run(session, origin, turn, `s${turn}`, createdAt);
51
+ }
52
+
53
+ test('L2_WINDOW is 20', () => {
54
+ assert.equal(L2_WINDOW, 20);
55
+ });
56
+
57
+ test('countDistinctBodyTurns: 2 ロール行 = 1 ターンとして数える', () => {
58
+ const db = makeDb();
59
+ insertTurn(db, { session: 'S', origin: 'S', turn: 1, createdAt: 100 });
60
+ insertTurn(db, { session: 'S', origin: 'S', turn: 2, createdAt: 200 });
61
+ assert.equal(countDistinctBodyTurns(db, 'S'), 2);
62
+ });
63
+
64
+ test('countDistinctBodyTurns: merge 跨ぎで origin が違うターンも別勘定', () => {
65
+ const db = makeDb();
66
+ // 前任 (origin=P) 15 ターン + 合流先 (origin=S) 10 ターン = 25
67
+ for (let i = 1; i <= 15; i++) {
68
+ insertTurn(db, { session: 'S', origin: 'P', turn: i, createdAt: i * 100 });
69
+ }
70
+ for (let i = 1; i <= 10; i++) {
71
+ insertTurn(db, { session: 'S', origin: 'S', turn: i, createdAt: 10000 + i * 100 });
72
+ }
73
+ assert.equal(countDistinctBodyTurns(db, 'S'), 25);
74
+ });
75
+
76
+ test('pickOldestUnsummarizedTurn: 全ターンが未要約なら created_at 最小を返す', () => {
77
+ const db = makeDb();
78
+ insertTurn(db, { session: 'S', origin: 'S', turn: 2, createdAt: 200 });
79
+ insertTurn(db, { session: 'S', origin: 'S', turn: 1, createdAt: 100 });
80
+ insertTurn(db, { session: 'S', origin: 'S', turn: 3, createdAt: 300 });
81
+ const oldest = pickOldestUnsummarizedTurn(db, 'S');
82
+ assert.equal(oldest?.turn_number, 1);
83
+ assert.equal(oldest?.origin_session_id, 'S');
84
+ assert.equal(oldest?.created_at, 100);
85
+ });
86
+
87
+ test('pickOldestUnsummarizedTurn: 既に要約済みのターンはスキップ', () => {
88
+ const db = makeDb();
89
+ insertTurn(db, { session: 'S', origin: 'S', turn: 1, createdAt: 100 });
90
+ insertTurn(db, { session: 'S', origin: 'S', turn: 2, createdAt: 200 });
91
+ insertTurn(db, { session: 'S', origin: 'S', turn: 3, createdAt: 300 });
92
+ insertSkeleton(db, { session: 'S', origin: 'S', turn: 1, createdAt: 100 });
93
+ insertSkeleton(db, { session: 'S', origin: 'S', turn: 2, createdAt: 200 });
94
+ const oldest = pickOldestUnsummarizedTurn(db, 'S');
95
+ assert.equal(oldest?.turn_number, 3);
96
+ });
97
+
98
+ test('pickOldestUnsummarizedTurn: 全部要約済みなら null', () => {
99
+ const db = makeDb();
100
+ insertTurn(db, { session: 'S', origin: 'S', turn: 1, createdAt: 100 });
101
+ insertSkeleton(db, { session: 'S', origin: 'S', turn: 1, createdAt: 100 });
102
+ assert.equal(pickOldestUnsummarizedTurn(db, 'S'), null);
103
+ });
104
+
105
+ test('pickOldestUnsummarizedTurn: merge 跨ぎで前任の最古ターンを優先', () => {
106
+ const db = makeDb();
107
+ // 前任 (origin=P) 15 ターン + 合流先 (origin=S) 10 ターン
108
+ for (let i = 1; i <= 15; i++) {
109
+ insertTurn(db, { session: 'S', origin: 'P', turn: i, createdAt: i * 100 });
110
+ }
111
+ for (let i = 1; i <= 10; i++) {
112
+ insertTurn(db, { session: 'S', origin: 'S', turn: i, createdAt: 10000 + i * 100 });
113
+ }
114
+ const oldest = pickOldestUnsummarizedTurn(db, 'S');
115
+ assert.equal(oldest?.origin_session_id, 'P');
116
+ assert.equal(oldest?.turn_number, 1);
117
+ assert.equal(oldest?.created_at, 100);
118
+ });
119
+
120
+ test('pickOldestUnsummarizedTurn: 同じ turn_number でも origin が違えば別扱い', () => {
121
+ const db = makeDb();
122
+ // 前任 turn 1 (未要約) と 合流先 turn 1 (要約済) が共存
123
+ insertTurn(db, { session: 'S', origin: 'P', turn: 1, createdAt: 100 });
124
+ insertTurn(db, { session: 'S', origin: 'S', turn: 1, createdAt: 500 });
125
+ insertSkeleton(db, { session: 'S', origin: 'S', turn: 1, createdAt: 500 });
126
+ const oldest = pickOldestUnsummarizedTurn(db, 'S');
127
+ assert.equal(oldest?.origin_session_id, 'P');
128
+ assert.equal(oldest?.turn_number, 1);
129
+ });
130
+
131
+ test('逐次要約シナリオ: 20 ターンまでは要約発火しない、21 ターン目で発火', () => {
132
+ const db = makeDb();
133
+ // 20 ターン投入
134
+ for (let i = 1; i <= 20; i++) {
135
+ insertTurn(db, { session: 'S', origin: 'S', turn: i, createdAt: i * 100 });
136
+ }
137
+ // 20 ターン時点: window を超えていないので要約しない
138
+ assert.equal(countDistinctBodyTurns(db, 'S') > L2_WINDOW, false);
139
+
140
+ // 21 ターン目投入
141
+ insertTurn(db, { session: 'S', origin: 'S', turn: 21, createdAt: 2100 });
142
+ assert.equal(countDistinctBodyTurns(db, 'S') > L2_WINDOW, true);
143
+
144
+ // 最古 = turn 1 が要約対象として選ばれる
145
+ const target1 = pickOldestUnsummarizedTurn(db, 'S');
146
+ assert.equal(target1?.turn_number, 1);
147
+
148
+ // turn 1 を要約済にして次のターンを模擬
149
+ insertSkeleton(db, { session: 'S', origin: 'S', turn: 1, createdAt: 100 });
150
+ insertTurn(db, { session: 'S', origin: 'S', turn: 22, createdAt: 2200 });
151
+
152
+ // 次は turn 2 が選ばれる
153
+ const target2 = pickOldestUnsummarizedTurn(db, 'S');
154
+ assert.equal(target2?.turn_number, 2);
155
+ });