@jjh32456/dsh-longlive 1.6.7 → 1.8.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.
- package/CHANGELOG.md +70 -1508
- package/LICENSE +1 -1
- package/QUICKSTART.md +34 -13
- package/README.en.md +18 -7
- package/README.md +34 -9
- package/SPEC.md +5 -5
- package/cordis.patch.yml +23 -9
- package/docs/ANSWER-GUARD.md +92 -0
- package/docs/ARCHITECTURE.md +4 -3
- package/docs/AUTO-RECALL-WIRING.md +1 -1
- package/docs/AUTO-RECALL.md +15 -9
- package/docs/CONFIG.md +168 -35
- package/docs/ERRORS.md +17 -5
- package/docs/FAQ.md +7 -7
- package/docs/FEATURES.md +96 -28
- package/docs/MIGRATION-1.6.0.md +2 -2
- package/docs/OPERATIONS.md +6 -6
- package/docs/PANEL-INTEGRATION.md +6 -0
- package/docs/PRIVACY.md +88 -0
- package/docs/QUICKSTART.md +5 -5
- package/docs/README.en.md +4 -2
- package/docs/RESIDENT-WATCHER.md +1 -1
- package/docs/SESSION-WATCH.md +6 -6
- package/docs/TROUBLESHOOTING.md +4 -4
- package/package.json +47 -117
- package/runbook.md +2 -2
- package/scripts/backfill-auto-cards.mjs +98 -0
- package/scripts/install-surgery-task.ps1 +5 -2
- package/scripts/lib/session-watch-fixture.mjs +1 -1
- package/scripts/longlive-doctor.mjs +6 -0
- package/scripts/migrate-180.mjs +163 -0
- package/scripts/recall-index-build.mjs +128 -0
- package/scripts/session-watch-drill.mjs +1 -1
- package/scripts/surgery-run.cmd +1 -1
- package/scripts/unattended-surgery.ps1 +5 -2
- package/scripts/verify-card-anchors.mjs +138 -0
- package/src/actions/audit.js +125 -0
- package/src/actions/evidence.js +122 -0
- package/src/actions/fs.js +162 -0
- package/src/activity/index.js +760 -0
- package/src/activity/model.js +351 -0
- package/src/activity/store.js +644 -0
- package/src/activity/thinking.js +176 -0
- package/src/adapters/constants.js +11 -0
- package/src/adapters/engram/data/aliases.json +51 -0
- package/src/adapters/engram/data/policy.json +8 -0
- package/src/adapters/engram/data/routing-rules.json +546 -0
- package/src/adapters/engram/index.js +235 -0
- package/src/adapters/registry.js +315 -0
- package/src/answer/audit.js +193 -0
- package/src/answer/evidence.js +30 -0
- package/src/answer/guard.js +1370 -0
- package/src/answer/index.js +1067 -0
- package/src/answer/query.js +111 -0
- package/src/answer/second-hop.mjs +101 -0
- package/src/auto-truncate.js +1 -1
- package/src/config.js +171 -47
- package/src/deps.js +6 -4
- package/src/features.js +182 -6
- package/src/heartbeat.js +156 -23
- package/src/identity/handover.js +52 -1
- package/src/identity/index.js +13 -5
- package/src/identity/inject-position.js +2 -2
- package/src/index.js +97 -7
- package/src/lifecycle/gap-clocks.js +328 -0
- package/src/lifecycle/index.js +269 -0
- package/src/live.js +22 -2
- package/src/memory/activity-capture.js +229 -0
- package/src/memory/anchors.js +194 -0
- package/src/memory/audit-records.js +222 -0
- package/src/memory/auto-cards.js +401 -0
- package/src/memory/capture.js +442 -0
- package/src/memory/card-anchors.js +203 -0
- package/src/memory/conflicts.js +106 -0
- package/src/memory/decay.js +311 -0
- package/src/memory/distill.js +471 -0
- package/src/memory/event-index.js +917 -0
- package/src/memory/evidence-chain.js +295 -0
- package/src/memory/evidence.js +145 -0
- package/src/memory/filter-runner.js +1 -1
- package/src/memory/filter.js +5 -1
- package/src/memory/forget-v2.js +410 -0
- package/src/memory/forget.js +71 -0
- package/src/memory/frames-activation.js +247 -0
- package/src/memory/fts-query.js +174 -0
- package/src/memory/governance.js +604 -0
- package/src/memory/index.js +1 -0
- package/src/memory/l2-gate.js +169 -0
- package/src/memory/lock.js +121 -0
- package/src/memory/migrate.js +139 -0
- package/src/memory/recall-v2.js +223 -0
- package/src/memory/recall.js +65 -2
- package/src/memory/records.js +425 -0
- package/src/memory/retention.js +128 -0
- package/src/memory/review.js +124 -0
- package/src/memory/trust.js +88 -0
- package/src/memory/version-chain.js +264 -0
- package/src/memory/wal.js +137 -0
- package/src/memory/working-frame.js +516 -0
- package/src/metrics/classify.mjs +5 -3
- package/src/metrics/index.mjs +21 -1
- package/src/metrics/prestep-latency.mjs +300 -0
- package/src/metrics/store.mjs +25 -0
- package/src/policy-profile.js +18 -3
- package/src/recall/anaphora.mjs +87 -0
- package/src/recall/cards.mjs +7 -0
- package/src/recall/embedding.mjs +8 -8
- package/src/recall/entities.mjs +112 -0
- package/src/recall/index.js +235 -19
- package/src/recall/inject-order.mjs +10 -8
- package/src/recall/privacy.mjs +105 -16
- package/src/recall/recall-core.mjs +116 -10
- package/src/recall/shared-visibility.js +302 -0
- package/src/recall/time-normalizer.mjs +196 -0
- package/src/role/activation.js +191 -0
- package/src/role/attention.js +318 -0
- package/src/role/base.js +36 -0
- package/src/role/channels.js +423 -0
- package/src/role/index.js +425 -0
- package/src/role/interfaces.js +115 -0
- package/src/role/personality.js +370 -0
- package/src/role/readers.js +151 -0
- package/src/role/relationships.js +477 -0
- package/src/role/store.js +202 -0
- package/src/role/writeprotect.js +373 -0
- package/src/router/aliases.json +176 -25
- package/src/router/background/agent-runner.mjs +1 -1
- package/src/router/background/anchor.mjs +1 -1
- package/src/router/background/audit.mjs +2 -2
- package/src/router/background/budget-ledger.mjs +2 -2
- package/src/router/background/host-sim.mjs +1 -1
- package/src/router/background/memory.mjs +4 -4
- package/src/router/background/queue.mjs +1 -1
- package/src/router/background/retry.mjs +1 -1
- package/src/router/background/router.mjs +1 -1
- package/src/router/background/stream.mjs +1 -1
- package/src/router/background/tool-policy.mjs +1 -1
- package/src/router/design-headings.json +5 -5
- package/src/router/face/config.mjs +1 -1
- package/src/router/face/identity.mjs +3 -3
- package/src/router/judge.mjs +8 -7
- package/src/router/policies/generic.json +21 -4
- package/src/router/route.js +109 -22
- package/src/router/router.mjs +10 -6
- package/src/router/routing-rules.json +260 -445
- package/src/router/run-sim.mjs +8 -8
- package/src/router/runtime.js +23 -12
- package/src/router/service/board.mjs +95 -15
- package/src/router/service/config.mjs +5 -5
- package/src/router/service/context.mjs +1 -1
- package/src/router/service/hint.mjs +14 -0
- package/src/router/service/queue.mjs +1 -1
- package/src/router/service/service.mjs +27 -11
- package/src/router/shadow.mjs +4 -3
- package/src/surgery-recovery.js +2 -2
- package/src/tool-output.js +2 -2
- package/src/tools.js +39 -7
- package/src/truncate-visible.js +2 -2
- package/src/ui/actions.js +1312 -0
- package/src/ui/panel-api.js +367 -0
- package/src/ui/panel-auth.js +251 -0
- package/src/ui/panel-integrity.js +243 -0
- package/src/ui/routes-readonly.js +695 -0
- package/src/ui/routes.js +3 -0
- package/docs/DEV.md +0 -279
- package/docs/PRESENCE-BUS-WIRING.md +0 -491
- package/docs/PRESENCE-BUS.md +0 -694
- package/docs/SURGERY.md +0 -186
- package/scripts/mark-core-cards.mjs +0 -289
- package/scripts/pack.mjs +0 -392
- package/scripts/router-proposals.mjs +0 -386
- package/src/router/fixtures/M1.json +0 -16
- package/src/router/fixtures/M2.json +0 -16
- package/src/router/fixtures/M3.json +0 -20
- package/src/router/fixtures/M4.json +0 -20
- package/src/router/fixtures/M5.json +0 -45
- package/src/router/fixtures/M6.json +0 -53
- package/src/router/fixtures/M7.json +0 -46
- package/src/router/fixtures/S1.json +0 -20
- package/src/router/fixtures/S2.json +0 -23
- package/src/router/fixtures/S3.json +0 -20
- package/src/router/fixtures/S4.json +0 -34
- package/src/router/fixtures/S5.json +0 -43
- package/src/router/fixtures/S6.json +0 -47
- package/src/router/fixtures/T1.json +0 -23
- package/src/router/fixtures/T2.json +0 -16
- package/src/router/fixtures/T3.json +0 -19
- package/src/router/fixtures/T4.json +0 -16
- package/src/router/fixtures/X1.json +0 -17
- package/src/router/fixtures/X2.json +0 -20
- package/src/router/gen-design-headings.mjs +0 -107
- package/src/router/policies/owner-rulings-2026-11-03.json +0 -62
- package/test/auto-recall.test.mjs +0 -474
- package/test/auto-truncate.test.mjs +0 -931
- package/test/autobind.test.mjs +0 -385
- package/test/bus.test.mjs +0 -1572
- package/test/cli-guard.test.mjs +0 -198
- package/test/conclusion.test.mjs +0 -408
- package/test/config-schema.test.mjs +0 -204
- package/test/d6-output.test.mjs +0 -194
- package/test/d6.test.mjs +0 -395
- package/test/deps.test.mjs +0 -342
- package/test/doctor-setup.test.mjs +0 -297
- package/test/embedding-recall.test.mjs +0 -347
- package/test/feature-flags.test.mjs +0 -568
- package/test/filter-shadow.test.mjs +0 -170
- package/test/fixtures/baseline-0.8.0-registration.json +0 -556
- package/test/fixtures/d6-header-tools.json +0 -1
- package/test/fixtures/lock-child.mjs +0 -67
- package/test/fixtures/pre-step-container.mjs +0 -100
- package/test/fixtures/privacy-memory/topics/pii-unmarked.md +0 -9
- package/test/fixtures/privacy-memory/topics/secret-local-only.md +0 -11
- package/test/fixtures/profile-session-longlive-config.json +0 -20
- package/test/fixtures/proposal-cas-child.mjs +0 -66
- package/test/host-engine-status.test.mjs +0 -307
- package/test/hot-truncate.test.mjs +0 -1349
- package/test/identity-card-marker.test.mjs +0 -307
- package/test/identity-router/run.mjs +0 -721
- package/test/integration-pre-step.test.mjs +0 -553
- package/test/live.test.mjs +0 -1020
- package/test/maintenance-schedule.test.mjs +0 -538
- package/test/memory.test.mjs +0 -2828
- package/test/metrics.test.mjs +0 -478
- package/test/orchestrator.test.mjs +0 -266
- package/test/p1_probe.mjs +0 -97
- package/test/packaging.test.mjs +0 -182
- package/test/portability.test.mjs +0 -343
- package/test/rc4-regressions.test.mjs +0 -562
- package/test/rc5-regressions.test.mjs +0 -197
- package/test/rc6-chinese.test.mjs +0 -394
- package/test/router-1100/apply-off.test.mjs +0 -175
- package/test/router-1100/budget-reconcile.test.mjs +0 -215
- package/test/router-1100/config-context.test.mjs +0 -211
- package/test/router-1100/helpers.mjs +0 -129
- package/test/router-1100/hint-envelope.test.mjs +0 -150
- package/test/router-1100/lock-crossproc.test.mjs +0 -206
- package/test/router-1100/proposal-cas-crossproc.test.mjs +0 -184
- package/test/router-1100/router-apply.test.mjs +0 -372
- package/test/router-1100/router-fixb.test.mjs +0 -602
- package/test/router-1100/router-proposals.test.mjs +0 -405
- package/test/router-1100/router-queue-lease-wal.test.mjs +0 -355
- package/test/router-1100/router-security-cr.test.mjs +0 -333
- package/test/router-1100/service-lifecycle.test.mjs +0 -220
- package/test/router-1100/tools-contract.test.mjs +0 -164
- package/test/router-1100/turn-reflect-tick.test.mjs +0 -89
- package/test/router-1100/v6-contract.test.mjs +0 -158
- package/test/router-1100/v6b-contract.test.mjs +0 -293
- package/test/router-1100/v6c-contract.test.mjs +0 -249
- package/test/router-1100/wal-rotation.test.mjs +0 -375
- package/test/router-1100/wp1-config.test.mjs +0 -170
- package/test/router-1100/wp1-default-off.test.mjs +0 -77
- package/test/router-1100/wp1-model-status.test.mjs +0 -90
- package/test/router-1100/wp1-status.test.mjs +0 -220
- package/test/router-1100/wp1-tools.test.mjs +0 -146
- package/test/router-1100/wp5-cli.test.mjs +0 -240
- package/test/router-1100/wp5-model-auto.test.mjs +0 -418
- package/test/router-1100/wp6-merge.test.mjs +0 -386
- package/test/router-1100/wp6-wake.test.mjs +0 -502
- package/test/router-background/audit.test.mjs +0 -37
- package/test/router-background/budget-ledger.test.mjs +0 -330
- package/test/router-background/cross-process.test.mjs +0 -222
- package/test/router-background/fixtures/cross-child.mjs +0 -85
- package/test/router-background/fixtures/real-shapes.mjs +0 -239
- package/test/router-background/router.test.mjs +0 -1044
- package/test/router-background/stream-retry.test.mjs +0 -358
- package/test/router-background/tool-agent.test.mjs +0 -384
- package/test/router-face/bridge.test.mjs +0 -169
- package/test/router-face/face.test.mjs +0 -316
- package/test/router-failclosed.test.mjs +0 -466
- package/test/router-identity.test.mjs +0 -384
- package/test/router-l1-l2.contract.test.mjs +0 -1075
- package/test/router-memory-tools.test.mjs +0 -262
- package/test/router-policy.test.mjs +0 -225
- package/test/router-runtime.test.mjs +0 -438
- package/test/router-shadow.test.mjs +0 -287
- package/test/router.test.mjs +0 -903
- package/test/run-rc.mjs +0 -299
- package/test/run.mjs +0 -2469
- package/test/safety-defaults.test.mjs +0 -179
- package/test/seeded-load.test.mjs +0 -436
- package/test/session-original.test.mjs +0 -338
- package/test/surgery-recovery.test.mjs +0 -936
- package/test/t7_drive.mjs +0 -151
- package/test/tool-output.test.mjs +0 -393
- package/test/tool-schema.test.mjs +0 -177
- package/test/tools_contract.test.mjs +0 -716
- package/test/truncate-visible.test.mjs +0 -1150
- package/test/ui-flags.test.mjs +0 -477
- package/test/v4-hardening.test.mjs +0 -427
|
@@ -1,405 +0,0 @@
|
|
|
1
|
-
// WP3 · proposal data layer contract (DESIGN §3.3/§3.6/§5.1/§5.2/§5.7/§5.10).
|
|
2
|
-
//
|
|
3
|
-
// These tests use a real RouterService (mock LLM, temp memory root) so the tool
|
|
4
|
-
// wrapper, caller resolution, audit file and proposal store are exercised
|
|
5
|
-
// together. They never touch the real memory root or the network.
|
|
6
|
-
|
|
7
|
-
import assert from 'node:assert/strict';
|
|
8
|
-
import crypto from 'node:crypto';
|
|
9
|
-
import fs from 'node:fs';
|
|
10
|
-
import path from 'node:path';
|
|
11
|
-
import test from 'node:test';
|
|
12
|
-
|
|
13
|
-
import { apply } from '../../src/index.js';
|
|
14
|
-
import { MockLlm } from '../../src/router/background/mock-llm.mjs';
|
|
15
|
-
import { createRouterTools } from '../../src/router/service/tools.mjs';
|
|
16
|
-
import { auditKinds, startedService, tempRoot } from './helpers.mjs';
|
|
17
|
-
|
|
18
|
-
function sha256(text) {
|
|
19
|
-
return crypto.createHash('sha256').update(String(text ?? ''), 'utf8').digest('hex');
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function execFor(sessionId, extra = {}) {
|
|
23
|
-
return { agent: { session: { header: { id: sessionId } } }, ...extra };
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function toolsOf(service) {
|
|
27
|
-
return new Map(createRouterTools(service).map((tool) => [tool.name, tool]));
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
async function propose(service, args, sessionId = 's1') {
|
|
31
|
-
return toolsOf(service).get('router_propose').invoke(args, { exec: execFor(sessionId) });
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
async function viewDetail(service, id, caller) {
|
|
35
|
-
return service.proposalsView({ mode: 'detail', id }, { caller });
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function proposalFiles(env) {
|
|
39
|
-
const dir = path.join(env.vaultDir, 'proposals');
|
|
40
|
-
if (!fs.existsSync(dir)) return [];
|
|
41
|
-
return fs.readdirSync(dir).filter((name) => name.startsWith('p-') && name.endsWith('.json'));
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function readAudit(env) {
|
|
45
|
-
const file = path.join(env.vaultDir, 'audit.jsonl');
|
|
46
|
-
if (!fs.existsSync(file)) return [];
|
|
47
|
-
return fs.readFileSync(file, 'utf8').trim().split('\n').filter(Boolean).map((line) => JSON.parse(line));
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
async function withStarted(config, fn) {
|
|
51
|
-
const env = await startedService({ config });
|
|
52
|
-
try {
|
|
53
|
-
assert.equal(env.result.ok, true);
|
|
54
|
-
assert.equal(env.result.state, 'running');
|
|
55
|
-
return await fn(env);
|
|
56
|
-
} finally {
|
|
57
|
-
try { await env.service.stop('test'); } catch { /* best effort */ }
|
|
58
|
-
env.cleanup();
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
test('K1: propose queues a ticket without writing canonical memory or the proposal body to audit', async () => {
|
|
63
|
-
await withStarted({ proposalEnabled: true }, async (env) => {
|
|
64
|
-
const journalDir = path.join(env.memoryRoot, 'journal');
|
|
65
|
-
fs.mkdirSync(journalDir, { recursive: true });
|
|
66
|
-
const journal = path.join(journalDir, '2026-11.md');
|
|
67
|
-
fs.writeFileSync(journal, '# journal\n\n- existing\n', 'utf8');
|
|
68
|
-
const before = fs.readFileSync(journal, 'utf8');
|
|
69
|
-
const beforeSha = sha256(before);
|
|
70
|
-
|
|
71
|
-
const result = await propose(env.service, {
|
|
72
|
-
op: 'journal.append',
|
|
73
|
-
target: 'journal/2026-11.md',
|
|
74
|
-
title: 'WP3 K1',
|
|
75
|
-
content: 'canonical must not move',
|
|
76
|
-
}, 's-author');
|
|
77
|
-
|
|
78
|
-
assert.equal(result.ok, true);
|
|
79
|
-
assert.equal(result.ticket, 1);
|
|
80
|
-
assert.match(result.ticketName, /^q-\d{4}$/u);
|
|
81
|
-
assert.equal(result.position, 1);
|
|
82
|
-
assert.deepEqual(result.ahead, []);
|
|
83
|
-
assert.equal(result.etaMs, 1500);
|
|
84
|
-
assert.equal(result.risk, 'low');
|
|
85
|
-
assert.equal(typeof result.preview, 'object');
|
|
86
|
-
|
|
87
|
-
const after = fs.readFileSync(journal, 'utf8');
|
|
88
|
-
assert.equal(after, before);
|
|
89
|
-
assert.equal(sha256(after), beforeSha);
|
|
90
|
-
|
|
91
|
-
const files = proposalFiles(env);
|
|
92
|
-
assert.equal(files.length, 1);
|
|
93
|
-
const proposal = JSON.parse(fs.readFileSync(path.join(env.vaultDir, 'proposals', files[0]), 'utf8'));
|
|
94
|
-
assert.equal(proposal.status, 'pending');
|
|
95
|
-
assert.equal(proposal.content, 'canonical must not move');
|
|
96
|
-
assert.equal(proposal.queue.ticket, result.ticketName);
|
|
97
|
-
assert.equal(proposal.origin.sessionId, 's-author');
|
|
98
|
-
assert.equal(proposal.queue.priority, 'P3');
|
|
99
|
-
assert.equal(proposal.queue.priorityBy, 'default');
|
|
100
|
-
|
|
101
|
-
const created = readAudit(env).find((line) => line.kind === 'proposal.create');
|
|
102
|
-
assert.ok(created, 'proposal.create must be audited');
|
|
103
|
-
assert.equal(created.content, undefined);
|
|
104
|
-
assert.match(String(created.contentSha), /^sha256:[0-9a-f]{64}$/u);
|
|
105
|
-
assert.equal(readAudit(env).some((line) => JSON.stringify(line).includes('canonical must not move')), false);
|
|
106
|
-
});
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
test('K2: identical content dedupes to the existing ticket and writes no second proposal', async () => {
|
|
110
|
-
await withStarted({ proposalEnabled: true }, async (env) => {
|
|
111
|
-
fs.mkdirSync(path.join(env.memoryRoot, 'journal'), { recursive: true });
|
|
112
|
-
fs.writeFileSync(path.join(env.memoryRoot, 'journal', '2026-11.md'), '# j\n', 'utf8');
|
|
113
|
-
const args = {
|
|
114
|
-
op: 'journal.append',
|
|
115
|
-
target: 'journal/2026-11.md',
|
|
116
|
-
title: 'dedupe',
|
|
117
|
-
content: 'same body',
|
|
118
|
-
};
|
|
119
|
-
const first = await propose(env.service, args, 's1');
|
|
120
|
-
const second = await propose(env.service, { ...args, title: 'dedupe again' }, 's1');
|
|
121
|
-
assert.equal(second.ok, true);
|
|
122
|
-
assert.equal(second.ticket, first.ticket);
|
|
123
|
-
assert.equal(second.duplicateOf, first.id);
|
|
124
|
-
assert.equal(second.status, 'pending');
|
|
125
|
-
assert.equal(proposalFiles(env).length, 1);
|
|
126
|
-
});
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
test('K5: quota and per-file size limits fail closed', async () => {
|
|
130
|
-
await withStarted({ proposalEnabled: true, maxProposals: 20, proposalsPerSessionHour: 60 }, async (env) => {
|
|
131
|
-
fs.mkdirSync(path.join(env.memoryRoot, 'journal'), { recursive: true });
|
|
132
|
-
fs.writeFileSync(path.join(env.memoryRoot, 'journal', '2026-11.md'), '# j\n', 'utf8');
|
|
133
|
-
// Alternate targets so consecutive proposals are never queue-adjacent for
|
|
134
|
-
// the same target: this test measures the global quota, not §5.8 merging.
|
|
135
|
-
const targetFor = (index) => (index % 2 === 0 ? 'journal/2026-11.md' : 'journal/2026-12.md');
|
|
136
|
-
const base = {
|
|
137
|
-
op: 'journal.append',
|
|
138
|
-
target: 'journal/2026-11.md',
|
|
139
|
-
title: 'quota',
|
|
140
|
-
};
|
|
141
|
-
for (let index = 0; index < 20; index += 1) {
|
|
142
|
-
const created = await propose(env.service, {
|
|
143
|
-
...base,
|
|
144
|
-
target: targetFor(index),
|
|
145
|
-
title: `quota-${index}`,
|
|
146
|
-
content: `body-${index}`,
|
|
147
|
-
}, 's1');
|
|
148
|
-
assert.equal(created.ok, true, `proposal ${index} should be accepted`);
|
|
149
|
-
}
|
|
150
|
-
const over = await propose(env.service, { ...base, title: 'quota-over', content: 'over' }, 's1');
|
|
151
|
-
assert.equal(over.ok, false);
|
|
152
|
-
assert.equal(over.code, 'E_PROPOSAL_QUOTA');
|
|
153
|
-
assert.equal(proposalFiles(env).length, 20);
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
await withStarted({ proposalEnabled: true }, async (env) => {
|
|
157
|
-
fs.mkdirSync(path.join(env.memoryRoot, 'journal'), { recursive: true });
|
|
158
|
-
fs.writeFileSync(path.join(env.memoryRoot, 'journal', '2026-11.md'), '# j\n', 'utf8');
|
|
159
|
-
const oversized = await propose(env.service, {
|
|
160
|
-
op: 'journal.append',
|
|
161
|
-
target: 'journal/2026-11.md',
|
|
162
|
-
title: 'oversized',
|
|
163
|
-
content: 'x'.repeat(4001),
|
|
164
|
-
}, 's1');
|
|
165
|
-
assert.equal(oversized.ok, false);
|
|
166
|
-
assert.equal(oversized.code, 'E_CONTENT_TOO_LARGE');
|
|
167
|
-
assert.equal(proposalFiles(env).length, 0);
|
|
168
|
-
});
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
test('K10: tool-channel P0 is denied, audited, and creates no proposal', async () => {
|
|
172
|
-
await withStarted({ proposalEnabled: true }, async (env) => {
|
|
173
|
-
fs.mkdirSync(path.join(env.memoryRoot, 'journal'), { recursive: true });
|
|
174
|
-
fs.writeFileSync(path.join(env.memoryRoot, 'journal', '2026-11.md'), '# j\n', 'utf8');
|
|
175
|
-
const denied = await propose(env.service, {
|
|
176
|
-
op: 'journal.append',
|
|
177
|
-
target: 'journal/2026-11.md',
|
|
178
|
-
title: 'P0',
|
|
179
|
-
content: 'owner only',
|
|
180
|
-
priority: 'P0',
|
|
181
|
-
}, 's-root');
|
|
182
|
-
assert.deepEqual(denied, { ok: false, code: 'E_PRIORITY_DENIED' });
|
|
183
|
-
assert.equal(proposalFiles(env).length, 0);
|
|
184
|
-
const rejected = readAudit(env).find((line) => line.kind === 'priority.rejected');
|
|
185
|
-
assert.ok(rejected, 'priority.rejected must be audited');
|
|
186
|
-
assert.equal(rejected.attempted, 'P0');
|
|
187
|
-
assert.equal(rejected.reason, 'tool-cannot-P0');
|
|
188
|
-
assert.equal(rejected.by, 's-root');
|
|
189
|
-
});
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
test('K11: detail is limited to author/main/owner; denial is audited; queue/list expose no body', async () => {
|
|
193
|
-
await withStarted({ proposalEnabled: true }, async (env) => {
|
|
194
|
-
fs.mkdirSync(path.join(env.memoryRoot, 'journal'), { recursive: true });
|
|
195
|
-
fs.writeFileSync(path.join(env.memoryRoot, 'journal', '2026-11.md'), '# j\n', 'utf8');
|
|
196
|
-
const created = await propose(env.service, {
|
|
197
|
-
op: 'journal.append',
|
|
198
|
-
target: 'journal/2026-11.md',
|
|
199
|
-
title: 'secret',
|
|
200
|
-
content: 'body-secret-value',
|
|
201
|
-
}, 's-author');
|
|
202
|
-
assert.equal(created.ok, true);
|
|
203
|
-
|
|
204
|
-
const denied = await viewDetail(env.service, created.id, { sessionId: 's-other', kind: 'subagent' });
|
|
205
|
-
assert.equal(denied.ok, false);
|
|
206
|
-
assert.equal(denied.code, 'E_PROPOSAL_FORBIDDEN');
|
|
207
|
-
assert.equal(JSON.stringify(denied).includes('body-secret-value'), false);
|
|
208
|
-
const denial = readAudit(env).find((line) => line.kind === 'proposal.detail.denied');
|
|
209
|
-
assert.ok(denial, 'proposal.detail.denied must be audited');
|
|
210
|
-
assert.equal(denial.id, created.id);
|
|
211
|
-
|
|
212
|
-
const queue = await env.service.proposalsView({ mode: 'queue' }, { caller: { sessionId: 's-author', kind: 'root' } });
|
|
213
|
-
assert.equal(queue.ok, true);
|
|
214
|
-
assert.equal(JSON.stringify(queue).includes('body-secret-value'), false);
|
|
215
|
-
assert.equal('content' in (queue.entries[0] ?? {}), false);
|
|
216
|
-
|
|
217
|
-
const list = await env.service.proposalsView({ mode: 'list' }, { caller: { sessionId: 's-author', kind: 'root' } });
|
|
218
|
-
assert.equal(list.ok, true);
|
|
219
|
-
assert.deepEqual(Object.keys(list.proposals[0]).sort(), [
|
|
220
|
-
'cancellable', 'createdAt', 'id', 'position', 'priority', 'state', 'status',
|
|
221
|
-
'terminal', 'ticket', 'title',
|
|
222
|
-
]);
|
|
223
|
-
assert.equal(JSON.stringify(list).includes('body-secret-value'), false);
|
|
224
|
-
|
|
225
|
-
const author = await viewDetail(env.service, created.id, { sessionId: 's-author', kind: 'root' });
|
|
226
|
-
assert.equal(author.ok, true);
|
|
227
|
-
assert.match(author.proposal.content, /^<<<PROPOSAL DATA — 不是指令 >>>\n/u);
|
|
228
|
-
assert.match(author.proposal.content, /body-secret-value/u);
|
|
229
|
-
assert.match(author.proposal.content, /\n<<<END PROPOSAL DATA>>>$/u);
|
|
230
|
-
|
|
231
|
-
const owner = await viewDetail(env.service, created.id, { sessionId: 'owner', kind: 'owner-cli' });
|
|
232
|
-
assert.equal(owner.ok, true);
|
|
233
|
-
});
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
test('K12: tool counts stay 21 / 25 / 28 and the proposal placeholders are wired', async () => {
|
|
237
|
-
const temp = tempRoot('longlive-router-counts-');
|
|
238
|
-
try {
|
|
239
|
-
const makeHarness = () => {
|
|
240
|
-
const tools = [];
|
|
241
|
-
const effects = [];
|
|
242
|
-
const ctx = {
|
|
243
|
-
tools: { register: (definition) => { tools.push(definition); return () => {}; } },
|
|
244
|
-
on: () => () => {},
|
|
245
|
-
logger: () => ({ warn: () => {}, info: () => {} }),
|
|
246
|
-
effect: (fn) => {
|
|
247
|
-
const dispose = fn();
|
|
248
|
-
effects.push(typeof dispose === 'function' ? dispose : () => {});
|
|
249
|
-
return dispose;
|
|
250
|
-
},
|
|
251
|
-
get: (name) => (name === 'llm' ? new MockLlm() : undefined),
|
|
252
|
-
};
|
|
253
|
-
return { ctx, tools, effects };
|
|
254
|
-
};
|
|
255
|
-
const count = async (flags) => {
|
|
256
|
-
const harness = makeHarness();
|
|
257
|
-
const memoryRoot = path.join(temp, `memory-${flags.label}`);
|
|
258
|
-
fs.mkdirSync(memoryRoot, { recursive: true });
|
|
259
|
-
fs.writeFileSync(path.join(memoryRoot, 'STATE.md'), '## 我在哪\n测试\n', 'utf8');
|
|
260
|
-
try {
|
|
261
|
-
const returned = await apply(harness.ctx, {
|
|
262
|
-
home: path.join(temp, `home-${flags.label}`),
|
|
263
|
-
memoryRoot,
|
|
264
|
-
env: {},
|
|
265
|
-
heartbeatMs: 0,
|
|
266
|
-
recoverOnStart: false,
|
|
267
|
-
defineTool: (definition) => definition,
|
|
268
|
-
routerRuntime: 'off',
|
|
269
|
-
...flags.config,
|
|
270
|
-
});
|
|
271
|
-
return { returned, names: harness.tools.map((tool) => tool.name) };
|
|
272
|
-
} finally {
|
|
273
|
-
for (const dispose of harness.effects.reverse()) {
|
|
274
|
-
try { await dispose(); } catch { /* best effort */ }
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
};
|
|
278
|
-
|
|
279
|
-
const off = await count({ label: 'off', config: {} });
|
|
280
|
-
assert.equal(off.returned, 22);
|
|
281
|
-
assert.equal(off.names.filter((name) => name.startsWith('router_')).length, 0);
|
|
282
|
-
|
|
283
|
-
const on = await count({ label: 'on', config: { routerEnabled: true, routerProvider: 'mock', routerModel: 'mock-1', routerTickMs: 60000 } });
|
|
284
|
-
assert.equal(on.returned, 26);
|
|
285
|
-
assert.equal(on.names.filter((name) => name.startsWith('router_')).length, 4);
|
|
286
|
-
|
|
287
|
-
const proposals = await count({
|
|
288
|
-
label: 'proposals',
|
|
289
|
-
config: {
|
|
290
|
-
routerEnabled: true,
|
|
291
|
-
routerProvider: 'mock',
|
|
292
|
-
routerModel: 'mock-1',
|
|
293
|
-
routerTickMs: 60000,
|
|
294
|
-
routerProposalEnabled: true,
|
|
295
|
-
},
|
|
296
|
-
});
|
|
297
|
-
assert.equal(proposals.returned, 29);
|
|
298
|
-
assert.deepEqual(
|
|
299
|
-
proposals.names.filter((name) => name.startsWith('router_')).sort(),
|
|
300
|
-
['router_apply', 'router_ask', 'router_proposals', 'router_propose', 'router_report', 'router_status', 'router_tick'],
|
|
301
|
-
);
|
|
302
|
-
} finally {
|
|
303
|
-
fs.rmSync(temp, { recursive: true, force: true });
|
|
304
|
-
}
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
test('ticket allocation is serialized by issue.lock: concurrent proposes get unique tickets', async () => {
|
|
308
|
-
await withStarted({ proposalEnabled: true }, async (env) => {
|
|
309
|
-
fs.mkdirSync(path.join(env.memoryRoot, 'journal'), { recursive: true });
|
|
310
|
-
fs.writeFileSync(path.join(env.memoryRoot, 'journal', '2026-11.md'), '# j\n', 'utf8');
|
|
311
|
-
// Different targets: a same-target pair is allowed to coalesce (§5.8);
|
|
312
|
-
// this test is only about serialized ticket allocation.
|
|
313
|
-
const [a, b] = await Promise.all([
|
|
314
|
-
propose(env.service, { op: 'journal.append', target: 'journal/2026-11.md', title: 'a', content: 'alpha' }, 's1'),
|
|
315
|
-
propose(env.service, { op: 'journal.append', target: 'journal/2026-12.md', title: 'b', content: 'beta' }, 's2'),
|
|
316
|
-
]);
|
|
317
|
-
assert.equal(a.ok, true);
|
|
318
|
-
assert.equal(b.ok, true);
|
|
319
|
-
assert.notEqual(a.ticket, b.ticket);
|
|
320
|
-
assert.notEqual(a.id, b.id);
|
|
321
|
-
assert.equal(fs.existsSync(path.join(env.vaultDir, 'locks', 'issue.lock')), false);
|
|
322
|
-
const tickets = proposalFiles(env).map((file) => {
|
|
323
|
-
const proposal = JSON.parse(fs.readFileSync(path.join(env.vaultDir, 'proposals', file), 'utf8'));
|
|
324
|
-
return proposal.queue.ticket;
|
|
325
|
-
});
|
|
326
|
-
assert.equal(new Set(tickets).size, tickets.length);
|
|
327
|
-
});
|
|
328
|
-
});
|
|
329
|
-
|
|
330
|
-
test('deadlineAt is createdAt + 72h and titles are sanitized to one line', async () => {
|
|
331
|
-
await withStarted({ proposalEnabled: true, proposalTtlH: 72 }, async (env) => {
|
|
332
|
-
fs.mkdirSync(path.join(env.memoryRoot, 'journal'), { recursive: true });
|
|
333
|
-
fs.writeFileSync(path.join(env.memoryRoot, 'journal', '2026-11.md'), '# j\n', 'utf8');
|
|
334
|
-
const created = await propose(env.service, {
|
|
335
|
-
op: 'journal.append',
|
|
336
|
-
target: 'journal/2026-11.md',
|
|
337
|
-
title: 'a\u0000b\u0007c\nd',
|
|
338
|
-
content: 'ttl',
|
|
339
|
-
}, 's1');
|
|
340
|
-
assert.equal(created.ok, true);
|
|
341
|
-
const proposal = JSON.parse(fs.readFileSync(path.join(env.vaultDir, 'proposals', proposalFiles(env)[0]), 'utf8'));
|
|
342
|
-
assert.equal(proposal.title, 'a b c d');
|
|
343
|
-
assert.equal(/[\u0000-\u001f\u007f]/u.test(proposal.title), false);
|
|
344
|
-
assert.equal(Date.parse(proposal.queue.deadlineAt) - Date.parse(proposal.createdAt), 72 * 60 * 60 * 1000);
|
|
345
|
-
});
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
test('default-off proposals leave no proposals directory and refuse the tool surface', async () => {
|
|
349
|
-
await withStarted({ proposalEnabled: false }, async (env) => {
|
|
350
|
-
assert.equal(fs.existsSync(path.join(env.vaultDir, 'proposals')), false);
|
|
351
|
-
assert.equal(createRouterTools(env.service).length, 4);
|
|
352
|
-
const disabled = await env.service.proposalsView({ mode: 'list' });
|
|
353
|
-
assert.equal(disabled.ok, false);
|
|
354
|
-
assert.equal(disabled.code, 'E_PROPOSAL_DISABLED');
|
|
355
|
-
const proposal = await env.service.proposeProposal({
|
|
356
|
-
op: 'journal.append',
|
|
357
|
-
target: 'journal/2026-11.md',
|
|
358
|
-
title: 'off',
|
|
359
|
-
content: 'x',
|
|
360
|
-
}, { caller: { sessionId: 's1', kind: 'root' } });
|
|
361
|
-
assert.equal(proposal.ok, false);
|
|
362
|
-
assert.equal(proposal.code, 'E_PROPOSAL_DISABLED');
|
|
363
|
-
assert.equal(fs.existsSync(path.join(env.vaultDir, 'proposals')), false);
|
|
364
|
-
});
|
|
365
|
-
});
|
|
366
|
-
|
|
367
|
-
test('path escapes, unknown ops and missing state sections are rejected before allocation', async () => {
|
|
368
|
-
await withStarted({ proposalEnabled: true }, async (env) => {
|
|
369
|
-
fs.mkdirSync(path.join(env.memoryRoot, 'journal'), { recursive: true });
|
|
370
|
-
fs.writeFileSync(path.join(env.memoryRoot, 'journal', '2026-11.md'), '# j\n', 'utf8');
|
|
371
|
-
const escapes = await propose(env.service, {
|
|
372
|
-
op: 'journal.append', target: '../STATE.md', title: 'escape', content: 'x',
|
|
373
|
-
}, 's1');
|
|
374
|
-
assert.equal(escapes.ok, false);
|
|
375
|
-
assert.equal(escapes.code, 'E_PATH_DENIED');
|
|
376
|
-
const unknown = await propose(env.service, {
|
|
377
|
-
op: 'topic.patch', target: 'topics/test.md', title: 'unknown', content: 'x',
|
|
378
|
-
}, 's1');
|
|
379
|
-
assert.equal(unknown.ok, false);
|
|
380
|
-
assert.equal(unknown.code, 'E_BAD_STATE');
|
|
381
|
-
const missingSection = await propose(env.service, {
|
|
382
|
-
op: 'state.section', target: 'STATE.md', title: '不存在的节', content: 'x',
|
|
383
|
-
}, 's1');
|
|
384
|
-
assert.equal(missingSection.ok, false);
|
|
385
|
-
assert.equal(missingSection.code, 'E_TARGET_MISSING');
|
|
386
|
-
assert.equal(proposalFiles(env).length, 0);
|
|
387
|
-
});
|
|
388
|
-
});
|
|
389
|
-
|
|
390
|
-
test('status exposes proposal counts and a seq counter without proposal bodies', async () => {
|
|
391
|
-
await withStarted({ proposalEnabled: true }, async (env) => {
|
|
392
|
-
fs.mkdirSync(path.join(env.memoryRoot, 'journal'), { recursive: true });
|
|
393
|
-
fs.writeFileSync(path.join(env.memoryRoot, 'journal', '2026-11.md'), '# j\n', 'utf8');
|
|
394
|
-
const created = await propose(env.service, {
|
|
395
|
-
op: 'journal.append', target: 'journal/2026-11.md', title: 'status', content: 'status-body',
|
|
396
|
-
}, 's1');
|
|
397
|
-
assert.equal(created.ok, true);
|
|
398
|
-
const snapshot = env.service.status();
|
|
399
|
-
assert.equal(snapshot.queue.pending, 1);
|
|
400
|
-
assert.equal(snapshot.proposals.queue.depth, 1);
|
|
401
|
-
assert.equal(typeof snapshot.proposals.queue.seqCounter, 'number');
|
|
402
|
-
assert.equal(JSON.stringify(snapshot).includes('status-body'), false);
|
|
403
|
-
assert.equal(auditKinds(path.join(env.vaultDir, 'audit.jsonl')).includes('proposal.create'), true);
|
|
404
|
-
});
|
|
405
|
-
});
|