dsh-lite-memory 0.16.4 → 0.18.1

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.
@@ -575,4 +575,27 @@ const scopeOf = (dir) => store.scopeOf(dir);
575
575
  assert.match(value.text, /适用条件还没确认/);
576
576
  }
577
577
 
578
+ /* L22:自动续写轮没有新的真人消息,仍沿用本会话上一次的请求上下文读正文。 */
579
+ {
580
+ const dir = mkdir('continuation');
581
+ const scope = scopeOf(dir);
582
+ const saved = save(scope, { title: '口播发布清单', body: '按清单逐项确认。' });
583
+ state.transact(scope, {}, (draft) => {
584
+ const row = draft.entries.find((entry) => entry.id === saved.id);
585
+ row.current.conditions = [{ key: 'seriesId', value: '口播' }];
586
+ return { changed: true };
587
+ });
588
+ enable(scope, { seriesId: '口播' });
589
+ const runtime = boot();
590
+ const agent = makeAgent('a-continuation', dir);
591
+ await runtime.preStep({ agent, signal: signal(), turn: 't1' }, enter(human('这期口播怎么做')));
592
+ const first = await runtime.exec('memory_read', { id: saved.id }, agent);
593
+ await runtime.preStep({ agent, signal: signal(), turn: 't2' }, enter());
594
+ assert.equal(first.status, 'ok');
595
+ assert.match(first.body, /按清单逐项确认/);
596
+ const second = await runtime.exec('memory_read', { id: saved.id }, agent);
597
+ assert.equal(second.status, 'ok');
598
+ assert.equal(second.repeat, true);
599
+ }
600
+
578
601
  console.log('evolution-read.test.mjs: all passed');
@@ -0,0 +1,136 @@
1
+ import assert from 'node:assert/strict';
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+
6
+ process.env.DSH_HOME = fs.mkdtempSync(path.join(os.tmpdir(), 'lite-memory-ha-'));
7
+ const store = await import('../store.js');
8
+ const state = await import('../state.js');
9
+
10
+ let passed = 0;
11
+ const test = (name, fn) => {
12
+ try {
13
+ fn();
14
+ passed += 1;
15
+ } catch (error) {
16
+ console.error(`FAIL ${name}`);
17
+ throw error;
18
+ }
19
+ };
20
+
21
+ const dirs = [];
22
+ const fresh = (label) => {
23
+ const dir = path.join(os.tmpdir(), `lite-memory-ha-${label}-${Math.random().toString(16).slice(2, 8)}`);
24
+ fs.mkdirSync(dir, { recursive: true });
25
+ dirs.push(dir);
26
+ return store.scopeOf(dir);
27
+ };
28
+
29
+ const lockOf = (scope) => path.join(scope.dir, '.lock');
30
+ const currentOf = (scope) => path.join(scope.dir, 'state.json');
31
+ const prevOf = (scope) => path.join(scope.dir, 'state.prev.json');
32
+
33
+ test('P1-C: a lock left by a dead pid is reclaimed instead of reported busy', () => {
34
+ const scope = fresh('stale-lock');
35
+ fs.mkdirSync(scope.dir, { recursive: true });
36
+ fs.writeFileSync(lockOf(scope), '999999:deadbeef');
37
+ const saved = store.upsert(scope, { title: '锁回收', summary: '崩溃进程留下的锁不该卡住写入', body: '正文' });
38
+ assert.ok(saved.record.id.startsWith('mem_'));
39
+ assert.equal(fs.existsSync(lockOf(scope)), false);
40
+ });
41
+
42
+ test('P1-C: a lock held by a live pid still reports busy and is never deleted', () => {
43
+ const scope = fresh('live-lock');
44
+ fs.mkdirSync(scope.dir, { recursive: true });
45
+ const file = lockOf(scope);
46
+ fs.writeFileSync(file, `${process.pid}:live`);
47
+ try {
48
+ assert.throws(
49
+ () => store.upsert(scope, { title: '忙', summary: '另一个进程正在写', body: '正文' }),
50
+ (error) => error.code === 'busy' && /另一个进程/.test(error.message),
51
+ );
52
+ assert.equal(fs.readFileSync(file, 'utf8'), `${process.pid}:live`);
53
+ } finally {
54
+ fs.rmSync(file, { force: true });
55
+ }
56
+ });
57
+
58
+ test('P0-B: a corrupt state.json falls back to state.prev.json and says so', () => {
59
+ const scope = fresh('recover');
60
+ store.upsert(scope, { title: '第一条', summary: '先写一条', body: '正文一' });
61
+ store.upsert(scope, { title: '第二条', summary: '再写一条,prev 才有货', body: '正文二' });
62
+ assert.ok(fs.existsSync(prevOf(scope)), 'second publish should have produced state.prev.json');
63
+ fs.writeFileSync(currentOf(scope), '{"format":"broken"');
64
+ const loaded = state.load(scope);
65
+ assert.equal(loaded.recovered, true);
66
+ assert.ok(loaded.state.entries.length >= 1);
67
+ assert.match(loaded.recovery.message, /已从上一版恢复/);
68
+ });
69
+
70
+ test('P0-B: both files unreadable gives a recovery step, not just a path', () => {
71
+ const scope = fresh('both-bad');
72
+ store.upsert(scope, { title: '第一条', summary: '先写一条', body: '正文一' });
73
+ store.upsert(scope, { title: '第二条', summary: '再写一条', body: '正文二' });
74
+ fs.writeFileSync(currentOf(scope), 'broken');
75
+ fs.writeFileSync(prevOf(scope), 'broken too');
76
+ try {
77
+ state.load(scope);
78
+ assert.fail('expected a corrupt error');
79
+ } catch (error) {
80
+ assert.equal(error.code, 'corrupt');
81
+ assert.match(error.message, /恢复/);
82
+ assert.match(error.message, /state\.prev\.json/);
83
+ }
84
+ });
85
+
86
+ test('P0-A: writeDurable publishes atomically and leaves no temp file behind', () => {
87
+ const dir = path.join(os.tmpdir(), `lite-memory-durable-${Math.random().toString(16).slice(2, 8)}`);
88
+ fs.mkdirSync(dir, { recursive: true });
89
+ dirs.push(dir);
90
+ const file = path.join(dir, 'x.json');
91
+ state.writeDurable(file, '{"ok":true}');
92
+ assert.equal(fs.readFileSync(file, 'utf8'), '{"ok":true}');
93
+ assert.deepEqual(fs.readdirSync(dir), ['x.json']);
94
+ state.writeDurable(file, 'second');
95
+ assert.equal(fs.readFileSync(file, 'utf8'), 'second');
96
+ });
97
+
98
+ test('P0-A: a failing write surfaces as unavailable and does not clobber the good file', () => {
99
+ const dir = path.join(os.tmpdir(), `lite-memory-durable-bad-${Math.random().toString(16).slice(2, 8)}`);
100
+ fs.mkdirSync(dir, { recursive: true });
101
+ dirs.push(dir);
102
+ const file = path.join(dir, 'x.json');
103
+ state.writeDurable(file, 'good');
104
+ const frozen = path.join(dir, 'frozen');
105
+ fs.mkdirSync(frozen);
106
+ fs.chmodSync(frozen, 0o500);
107
+ try {
108
+ assert.throws(
109
+ () => state.writeDurable(path.join(frozen, 'y.json'), 'nope'),
110
+ (error) => error.code === 'unavailable' && /写不进去/.test(error.message),
111
+ );
112
+ } finally {
113
+ fs.chmodSync(frozen, 0o700);
114
+ }
115
+ assert.equal(fs.readFileSync(file, 'utf8'), 'good');
116
+ });
117
+
118
+ test('P1-D: archived body round-trips through cold storage, and corrupt cold fails loudly', () => {
119
+ const scope = fresh('cold');
120
+ const saved = store.upsert(scope, { title: '冷存', summary: '归档正文放冷存', body: '归档正文内容' });
121
+ const archived = store.forget(scope, saved.record.id, { mode: 'archive' });
122
+ assert.ok(['archive', 'archived'].includes(archived.mode));
123
+ const cold = path.join(scope.dir, 'cold', `${saved.record.id}.json`);
124
+ assert.ok(fs.existsSync(cold), 'cold file should exist for an archived record');
125
+ assert.equal(JSON.parse(fs.readFileSync(cold, 'utf8')).body, '归档正文内容');
126
+ assert.equal(store.readEntry(scope, saved.record.id).body, '归档正文内容');
127
+ fs.writeFileSync(cold, '{broken');
128
+ assert.throws(
129
+ () => store.readEntry(scope, saved.record.id),
130
+ (error) => error.code === 'cold_corrupt' && /冷存/.test(error.message),
131
+ 'a broken cold file must never read back as an empty body',
132
+ );
133
+ });
134
+
135
+ for (const dir of dirs) fs.rmSync(dir, { recursive: true, force: true });
136
+ console.log(`test/ha.test.mjs: ${passed} passed`);