@hypercarrier/rarebit 0.1.0-alpha.6 → 0.2.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.
- package/CHANGELOG.md +33 -3
- package/README.md +296 -122
- package/RUNBOOK.md +16 -9
- package/docs/examples/rarebit.settings.json +19 -0
- package/package.json +9 -2
- package/schemas/rarebit.schema.json +2 -1
- package/scripts/e2e-recap-pi.mjs +458 -0
- package/scripts/e2e-settings-pi.mjs +267 -0
- package/scripts/e2e-v020-pi.mjs +271 -0
- package/scripts/experiment-recap-fullscreen.mjs +56 -0
- package/scripts/verify-package.mjs +14 -8
- package/src/extension.mjs +246 -24
- package/src/lifecycle.mjs +32 -7
- package/src/rarebit-artifact-state.mjs +8 -12
- package/src/rarebit-cli.mjs +2 -0
- package/src/rarebit-command.mjs +26 -1
- package/src/rarebit-core.mjs +122 -23
- package/src/rarebit-read-checkpoint.mjs +59 -0
- package/src/rarebit-recap.mjs +479 -0
- package/src/rarebit-service.mjs +65 -9
- package/src/rarebit-settings-ui.mjs +479 -0
- package/src/rarebit-settings.mjs +90 -7
- package/src/types.d.ts +65 -5
- package/test/lifecycle-contract-independent.test.mjs +2 -0
- package/test/package-artifact-identity.test.mjs +54 -0
- package/test/rarebit-artifact-state.test.mjs +10 -4
- package/test/rarebit-command.test.mjs +11 -4
- package/test/rarebit-core.test.mjs +50 -0
- package/test/rarebit-recap.test.mjs +662 -0
- package/test/rarebit-settings-ui.test.mjs +400 -0
- package/test/rarebit-sidecar-protocol.test.mjs +20 -0
- package/test/rarebit-v020.test.mjs +299 -0
- package/test/recap-read-checkpoint.test.mjs +126 -0
- package/test/summary-attention-independent.test.mjs +1 -1
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { test } from "node:test";
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
DEFAULT_RAREBIT_MAX_INPUT_TOKENS,
|
|
9
|
+
DEFAULT_RAREBIT_RECAP_TIMEZONE,
|
|
10
|
+
normalizeRarebitRecapTimezone,
|
|
11
|
+
resolveRarebitSettings,
|
|
12
|
+
} from "../src/rarebit-settings.mjs";
|
|
13
|
+
import {
|
|
14
|
+
DEFAULT_RAREBIT_MAX_PROMPT_CHARS,
|
|
15
|
+
processRarebitSummary,
|
|
16
|
+
} from "../src/rarebit-service.mjs";
|
|
17
|
+
import { createRarebitRecapController } from "../src/rarebit-recap.mjs";
|
|
18
|
+
import { selectRarebits, sha256 } from "../src/rarebit-core.mjs";
|
|
19
|
+
import { extractRarebitSynthesisReceipt } from "../src/rarebit-model.mjs";
|
|
20
|
+
import registerPiRarebit, { readConfiguredRarebitSettings } from "../src/extension.mjs";
|
|
21
|
+
import {
|
|
22
|
+
RAREBIT_SETTINGS_TABS,
|
|
23
|
+
openRarebitPalette,
|
|
24
|
+
openRarebitSettings,
|
|
25
|
+
readRarebitSettingsDocument,
|
|
26
|
+
saveRarebitSettingsDocument,
|
|
27
|
+
} from "../src/rarebit-settings-ui.mjs";
|
|
28
|
+
import { parseRarebitCommand } from "../src/rarebit-command.mjs";
|
|
29
|
+
|
|
30
|
+
async function fixture(initial = {}) {
|
|
31
|
+
const root = await mkdtemp(join(tmpdir(), "rarebit-settings-ui-"));
|
|
32
|
+
const cwd = join(root, "project");
|
|
33
|
+
const agentDir = join(root, "agent");
|
|
34
|
+
const settingsFile = join(agentDir, "settings.json");
|
|
35
|
+
const projectSettings = join(cwd, ".pi", "settings.json");
|
|
36
|
+
await mkdir(agentDir, { recursive: true });
|
|
37
|
+
await mkdir(join(cwd, ".pi"), { recursive: true });
|
|
38
|
+
await writeFile(settingsFile, `${JSON.stringify(initial, null, 2)}\n`);
|
|
39
|
+
return { root, cwd, agentDir, settingsFile, projectSettings };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function theme() {
|
|
43
|
+
return {
|
|
44
|
+
bold: (value) => value,
|
|
45
|
+
fg: (_role, value) => value,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function context(box, ui, overrides = {}) {
|
|
50
|
+
return {
|
|
51
|
+
mode: "tui",
|
|
52
|
+
hasUI: true,
|
|
53
|
+
cwd: box.cwd,
|
|
54
|
+
isProjectTrusted: () => true,
|
|
55
|
+
ui,
|
|
56
|
+
...overrides,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
test("resolved settings keep diagnostics quiet by default and retain global values through project partial overrides", () => {
|
|
61
|
+
const resolved = resolveRarebitSettings(
|
|
62
|
+
{
|
|
63
|
+
rarebit: {
|
|
64
|
+
model: "fixture/summary",
|
|
65
|
+
diagnostics: { summary_triggered: true, summary_updated: false },
|
|
66
|
+
max_input_tokens: 64_000,
|
|
67
|
+
recap: { enabled: false, delay_ms: 10_000, timezone: "Asia/Hong_Kong" },
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
{ rarebit: { recap: { delay_ms: 2_000 } } },
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
assert.equal(resolved.diagnostics.summaryTriggered, true);
|
|
74
|
+
assert.equal(resolved.diagnostics.summaryUpdated, false);
|
|
75
|
+
assert.equal(resolved.recap.enabled, false);
|
|
76
|
+
assert.equal(resolved.recap.delayMs, 2_000);
|
|
77
|
+
assert.equal(resolved.recap.timezone, "Asia/Hong_Kong");
|
|
78
|
+
assert.equal(resolved.maxInputTokens, DEFAULT_RAREBIT_MAX_INPUT_TOKENS);
|
|
79
|
+
|
|
80
|
+
const defaults = resolveRarebitSettings({ rarebit: { model: "fixture/summary" } });
|
|
81
|
+
assert.deepEqual(defaults.diagnostics, { summaryTriggered: false, summaryUpdated: false });
|
|
82
|
+
assert.equal(defaults.recap.timezone, DEFAULT_RAREBIT_RECAP_TIMEZONE);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("the real settings loader preserves a global disabled Recap through a partial trusted-project override", async () => {
|
|
86
|
+
const box = await fixture({
|
|
87
|
+
rarebit: {
|
|
88
|
+
model: "fixture/summary",
|
|
89
|
+
diagnostics: { summary_triggered: false, summary_updated: false },
|
|
90
|
+
max_input_tokens: 64_000,
|
|
91
|
+
recap: { enabled: false, delay_ms: 60_000, timezone: "Asia/Hong_Kong" },
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
await writeFile(box.projectSettings, JSON.stringify({
|
|
95
|
+
other_extension: { preserve: true },
|
|
96
|
+
rarebit: { recap: { delay_ms: 2_000 } },
|
|
97
|
+
}, null, 2));
|
|
98
|
+
|
|
99
|
+
const effective = await readConfiguredRarebitSettings({
|
|
100
|
+
cwd: box.cwd,
|
|
101
|
+
agentDir: box.agentDir,
|
|
102
|
+
projectTrusted: true,
|
|
103
|
+
model: { provider: "fixture", id: "summary" },
|
|
104
|
+
});
|
|
105
|
+
assert.equal(effective.maxInputTokens, 64_000);
|
|
106
|
+
assert.deepEqual(effective.diagnostics, { summaryTriggered: false, summaryUpdated: false });
|
|
107
|
+
assert.equal(effective.recap.enabled, false);
|
|
108
|
+
assert.equal(effective.recap.delayMs, 2_000);
|
|
109
|
+
assert.equal(effective.recap.timezone, "Asia/Hong_Kong");
|
|
110
|
+
|
|
111
|
+
const projectDocument = await readRarebitSettingsDocument({
|
|
112
|
+
cwd: box.cwd,
|
|
113
|
+
agentDir: box.agentDir,
|
|
114
|
+
scope: "project",
|
|
115
|
+
projectTrusted: true,
|
|
116
|
+
});
|
|
117
|
+
await saveRarebitSettingsDocument(projectDocument, {
|
|
118
|
+
...projectDocument.namespace,
|
|
119
|
+
recap: { ...projectDocument.namespace.recap, timezone: "UTC" },
|
|
120
|
+
});
|
|
121
|
+
const global = JSON.parse(await readFile(box.settingsFile, "utf8"));
|
|
122
|
+
const project = JSON.parse(await readFile(box.projectSettings, "utf8"));
|
|
123
|
+
assert.equal(global.rarebit.recap.enabled, false);
|
|
124
|
+
assert.equal(global.rarebit.recap.timezone, "Asia/Hong_Kong");
|
|
125
|
+
assert.deepEqual(project.other_extension, { preserve: true });
|
|
126
|
+
assert.equal(project.rarebit.recap.delay_ms, 2_000);
|
|
127
|
+
assert.equal(project.rarebit.recap.timezone, "UTC");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("recap timezone accepts an IANA zone and falls back to host for invalid input", () => {
|
|
131
|
+
assert.equal(normalizeRarebitRecapTimezone("Asia/Hong_Kong"), "Asia/Hong_Kong");
|
|
132
|
+
assert.equal(normalizeRarebitRecapTimezone("not/a-timezone"), DEFAULT_RAREBIT_RECAP_TIMEZONE);
|
|
133
|
+
assert.equal(normalizeRarebitRecapTimezone(""), DEFAULT_RAREBIT_RECAP_TIMEZONE);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("summary service maps the configurable input-token limit to the persisted character cap", async () => {
|
|
137
|
+
const branch = [
|
|
138
|
+
{ id: "u1", type: "message", message: { role: "user", content: [{ type: "text", text: "A request" }] } },
|
|
139
|
+
{ id: "a1", type: "message", message: { role: "assistant", content: [{ type: "text", text: "A response" }] } },
|
|
140
|
+
];
|
|
141
|
+
const result = await processRarebitSummary(
|
|
142
|
+
{ sessionManager: { getBranch: () => branch } },
|
|
143
|
+
{ branch, maxInputTokens: 32_000, forceSynthesis: true },
|
|
144
|
+
);
|
|
145
|
+
assert.equal(result.record.inputCoveragePolicy.maxPromptChars, 128_000);
|
|
146
|
+
assert.equal(DEFAULT_RAREBIT_MAX_PROMPT_CHARS, DEFAULT_RAREBIT_MAX_INPUT_TOKENS * 4);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("recap renders the human label and converts the receipt timestamp to the configured timezone", async () => {
|
|
150
|
+
const branch = [
|
|
151
|
+
{ id: "u1", type: "message", message: { role: "user", content: [{ type: "text", text: "A request" }] } },
|
|
152
|
+
{ id: "a1", type: "message", message: { role: "assistant", content: [{ type: "text", text: "A response" }] } },
|
|
153
|
+
];
|
|
154
|
+
const selection = selectRarebits(branch);
|
|
155
|
+
const observedAt = "2026-01-01T00:00:00.000Z";
|
|
156
|
+
const model = { provider: "fixture", id: "summary" };
|
|
157
|
+
const receipt = {
|
|
158
|
+
schemaVersion: 4, type: "rarebit_summary", status: "ok", jobId: sha256("job-1"), sessionId: "session-1",
|
|
159
|
+
branch: { leafId: "a1", entryCount: 2, pathHash: sha256(["u1", "a1"]) }, observedAt,
|
|
160
|
+
selection: { manifestHash: selection.manifestHash, selectorVersion: selection.manifest.selectorVersion,
|
|
161
|
+
occurrenceCount: selection.occurrences.length, uniquePayloadCount: selection.payloads.length, latestUserSourceEntryId: "u1" },
|
|
162
|
+
lifecycleBoundary: "agent_settled", implementationVersion: "hc-rarebit-summary-v7", synthesisMode: "forced",
|
|
163
|
+
inputCoveragePolicy: { strategy: "newest_suffix_with_explicit_omission", maxPromptChars: 10_000 },
|
|
164
|
+
promptVersion: "rarebit-summary-v8", model, modelProvenance: { source: "test", status: "resolved" },
|
|
165
|
+
summary: "A current summary.", sessionStatus: "needs_attention", statusReason: "approval",
|
|
166
|
+
inputCoverage: { totalMessageCount: selection.occurrences.length, includedMessageCount: selection.occurrences.length, omittedMessageCount: 0, omittedTextChars: 0, promptChars: 100, complete: true },
|
|
167
|
+
synthesis: extractRarebitSynthesisReceipt({}, { requestedModel: model, startedAt: observedAt, completedAt: observedAt, durationMs: 0 }),
|
|
168
|
+
};
|
|
169
|
+
const widgets = [];
|
|
170
|
+
const context = {
|
|
171
|
+
mode: "tui",
|
|
172
|
+
hasUI: true,
|
|
173
|
+
sessionManager: {
|
|
174
|
+
getHeader: () => ({ id: "session-1" }),
|
|
175
|
+
getSessionFile: () => "/tmp/rarebit-settings-ui-session.jsonl",
|
|
176
|
+
getBranch: () => branch,
|
|
177
|
+
},
|
|
178
|
+
ui: { setWidget: (_key, lines) => widgets.push(lines) },
|
|
179
|
+
};
|
|
180
|
+
const controller = createRarebitRecapController({
|
|
181
|
+
timezone: "Asia/Hong_Kong",
|
|
182
|
+
readCurrent: async () => ({
|
|
183
|
+
receipt,
|
|
184
|
+
artifactState: {
|
|
185
|
+
syncState: "assessment_current",
|
|
186
|
+
applicability: "exact_selection",
|
|
187
|
+
receiptRef: { jobId: receipt.jobId },
|
|
188
|
+
projection: { status: "needs_attention" },
|
|
189
|
+
},
|
|
190
|
+
}),
|
|
191
|
+
});
|
|
192
|
+
const result = await controller.showExisting(context);
|
|
193
|
+
assert.equal(result.shown, true);
|
|
194
|
+
const heading = widgets[0]({ requestRender() {} }, { fg: (_name, text) => text }).render(200).find((line) => line.includes("Recap·")).trim();
|
|
195
|
+
assert.match(heading, /^(?:◆!)?Recap·needsyou·01\/01 08:00\(GMT\+8\)/);
|
|
196
|
+
assert.doesNotMatch(heading, /as of|2026|Asia\/Hong_Kong/);
|
|
197
|
+
assert.doesNotMatch(heading, /Rarebit Summary/);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test("quiet Summary diagnostics still report materialization failures", async () => {
|
|
201
|
+
const root = await mkdtemp(join(tmpdir(), "rarebit-settings-failure-"));
|
|
202
|
+
const handlers = new Map();
|
|
203
|
+
const commands = new Map();
|
|
204
|
+
const notices = [];
|
|
205
|
+
let providerCalls = 0;
|
|
206
|
+
registerPiRarebit(
|
|
207
|
+
{
|
|
208
|
+
on: (event, handler) => handlers.set(event, handler),
|
|
209
|
+
registerCommand: (name, command) => commands.set(name, command),
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
model: { provider: "fixture", id: "summary" },
|
|
213
|
+
summaryPolicy: { minTotalLength: 0, maxRarebitRatio: 1 },
|
|
214
|
+
sessionRoot: root,
|
|
215
|
+
rarebitRoot: join(root, "rarebit"),
|
|
216
|
+
queryAutomaticSummaryPolicy: async () => ({ decision: "abstain", queryStatus: "test" }),
|
|
217
|
+
modelClient: { complete: async () => { providerCalls += 1; throw new Error("synthetic provider failure"); } },
|
|
218
|
+
diagnostics: { summaryTriggered: false, summaryUpdated: false },
|
|
219
|
+
},
|
|
220
|
+
);
|
|
221
|
+
const branch = [
|
|
222
|
+
{ id: "u1", type: "message", message: { role: "user", content: [{ type: "text", text: "A request" }] } },
|
|
223
|
+
{ id: "a1", type: "message", message: { role: "assistant", content: [{ type: "text", text: "A response" }] } },
|
|
224
|
+
];
|
|
225
|
+
const context = {
|
|
226
|
+
mode: "tui",
|
|
227
|
+
cwd: root,
|
|
228
|
+
hasUI: true,
|
|
229
|
+
isProjectTrusted: () => true,
|
|
230
|
+
sessionManager: {
|
|
231
|
+
getHeader: () => ({ id: "session-failure" }),
|
|
232
|
+
getSessionFile: () => join(root, "session.jsonl"),
|
|
233
|
+
getBranch: () => branch,
|
|
234
|
+
},
|
|
235
|
+
ui: { notify: (text, level) => notices.push({ text, level }) },
|
|
236
|
+
};
|
|
237
|
+
handlers.get("session_start")({}, context);
|
|
238
|
+
const waitForFailure = async (count) => {
|
|
239
|
+
const deadline = Date.now() + 2_000;
|
|
240
|
+
while (Date.now() < deadline && notices.filter(({ text, level }) => level === "error" && /Summary failed/.test(text)).length < count)
|
|
241
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
242
|
+
assert.equal(notices.filter(({ text, level }) => level === "error" && /Summary failed/.test(text)).length, count);
|
|
243
|
+
};
|
|
244
|
+
await commands.get("rarebit").handler("summarize", context);
|
|
245
|
+
await waitForFailure(1);
|
|
246
|
+
await commands.get("rarebit").handler("summarize", context);
|
|
247
|
+
await waitForFailure(2);
|
|
248
|
+
assert.equal(providerCalls, 1, "the cached failure reuses the original provider outcome");
|
|
249
|
+
assert.equal(notices.some(({ text }) => /already current|updated and current/.test(text)), false);
|
|
250
|
+
assert.equal(notices.some(({ text }) => /Summary triggered/.test(text)), false);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test("settings document round-trip preserves Pi and other extension namespaces", async () => {
|
|
254
|
+
const box = await fixture({
|
|
255
|
+
theme: "dark",
|
|
256
|
+
another_extension: { enabled: true },
|
|
257
|
+
rarebit: {
|
|
258
|
+
model: "fixture/summary",
|
|
259
|
+
recap: { enabled: false, delay_ms: 60_000, timezone: "UTC" },
|
|
260
|
+
max_input_tokens: 64_000,
|
|
261
|
+
},
|
|
262
|
+
});
|
|
263
|
+
const document = await readRarebitSettingsDocument({ cwd: box.cwd, agentDir: box.agentDir, scope: "global" });
|
|
264
|
+
const saved = await saveRarebitSettingsDocument(document, {
|
|
265
|
+
...document.namespace,
|
|
266
|
+
max_input_tokens: 32_000,
|
|
267
|
+
recap: { ...document.namespace.recap, timezone: "Asia/Hong_Kong" },
|
|
268
|
+
});
|
|
269
|
+
const parsed = JSON.parse(await readFile(box.settingsFile, "utf8"));
|
|
270
|
+
assert.equal(parsed.theme, "dark");
|
|
271
|
+
assert.deepEqual(parsed.another_extension, { enabled: true });
|
|
272
|
+
assert.equal(parsed.rarebit.max_input_tokens, 32_000);
|
|
273
|
+
assert.equal(parsed.rarebit.recap.timezone, "Asia/Hong_Kong");
|
|
274
|
+
assert.notEqual(saved.revision, document.revision);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
test("settings cancel and invalid input leave the file unchanged and do not enter the Session", async () => {
|
|
278
|
+
const box = await fixture({
|
|
279
|
+
unrelated: { keep: "yes" },
|
|
280
|
+
rarebit: { model: "fixture/summary", max_input_tokens: 64_000 },
|
|
281
|
+
});
|
|
282
|
+
const before = await readFile(box.settingsFile, "utf8");
|
|
283
|
+
let customCalls = 0;
|
|
284
|
+
let modelCalls = 0;
|
|
285
|
+
let sessionWrites = 0;
|
|
286
|
+
const notifications = [];
|
|
287
|
+
const ui = {
|
|
288
|
+
custom: async (factory) => {
|
|
289
|
+
customCalls += 1;
|
|
290
|
+
if (customCalls > 2) return undefined;
|
|
291
|
+
let selected;
|
|
292
|
+
const component = factory({ requestRender() {} }, theme(), {}, (value) => { selected = value; });
|
|
293
|
+
assert.match(component.render(120).join("\n"), /Actions.*Summary.*Recap.*Session/);
|
|
294
|
+
if (customCalls === 1) {
|
|
295
|
+
component.handleInput("\t");
|
|
296
|
+
component.handleInput("\x1b[B");
|
|
297
|
+
component.handleInput("\x1b[B");
|
|
298
|
+
component.handleInput("\r");
|
|
299
|
+
return selected;
|
|
300
|
+
}
|
|
301
|
+
return undefined;
|
|
302
|
+
},
|
|
303
|
+
input: async () => "not/a-timezone",
|
|
304
|
+
select: async () => undefined,
|
|
305
|
+
confirm: async () => false,
|
|
306
|
+
notify: (message, level) => notifications.push({ message, level }),
|
|
307
|
+
};
|
|
308
|
+
await openRarebitSettings(context(box, ui, {
|
|
309
|
+
sendUserMessage: () => { modelCalls += 1; },
|
|
310
|
+
sessionManager: { appendMessage: () => { sessionWrites += 1; } },
|
|
311
|
+
}), { agentDir: box.agentDir, initialScope: "global" });
|
|
312
|
+
assert.equal(await readFile(box.settingsFile, "utf8"), before);
|
|
313
|
+
assert.equal(modelCalls, 0);
|
|
314
|
+
assert.equal(sessionWrites, 0);
|
|
315
|
+
assert.equal(notifications.length, 1);
|
|
316
|
+
assert.equal(notifications[0].level, "error");
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
test("settings confirmation cancel leaves the source file unchanged", async () => {
|
|
320
|
+
const box = await fixture({ rarebit: { model: "fixture/summary" } });
|
|
321
|
+
const before = await readFile(box.settingsFile, "utf8");
|
|
322
|
+
let customCalls = 0;
|
|
323
|
+
const ui = {
|
|
324
|
+
custom: async (factory) => {
|
|
325
|
+
customCalls += 1;
|
|
326
|
+
if (customCalls > 2) return undefined;
|
|
327
|
+
let selected;
|
|
328
|
+
const component = factory({ requestRender() {} }, theme(), {}, (value) => { selected = value; });
|
|
329
|
+
if (customCalls === 1) component.handleInput("\r");
|
|
330
|
+
return selected;
|
|
331
|
+
},
|
|
332
|
+
select: async () => "on",
|
|
333
|
+
confirm: async () => false,
|
|
334
|
+
notify: () => {},
|
|
335
|
+
};
|
|
336
|
+
await openRarebitSettings(context(box, ui), { agentDir: box.agentDir });
|
|
337
|
+
assert.equal(await readFile(box.settingsFile, "utf8"), before);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
test("settings UI exposes the tabbed Summary and Recap controls and saves a confirmed edit", async () => {
|
|
341
|
+
const box = await fixture({ rarebit: { model: "fixture/summary" } });
|
|
342
|
+
const rendered = [];
|
|
343
|
+
let customCalls = 0;
|
|
344
|
+
const ui = {
|
|
345
|
+
custom: async (factory) => {
|
|
346
|
+
customCalls += 1;
|
|
347
|
+
if (customCalls > 2) return undefined;
|
|
348
|
+
let selected;
|
|
349
|
+
const component = factory({ requestRender() {} }, theme(), {}, (value) => { selected = value; });
|
|
350
|
+
rendered.push(component.render(120).join("\n"));
|
|
351
|
+
if (customCalls === 1) {
|
|
352
|
+
component.handleInput("\r");
|
|
353
|
+
}
|
|
354
|
+
return selected;
|
|
355
|
+
},
|
|
356
|
+
select: async () => "on",
|
|
357
|
+
confirm: async () => true,
|
|
358
|
+
notify: () => {},
|
|
359
|
+
};
|
|
360
|
+
await openRarebitSettings(context(box, ui), { agentDir: box.agentDir });
|
|
361
|
+
assert.deepEqual(RAREBIT_SETTINGS_TABS, ["Actions", "Summary", "Recap", "Session"]);
|
|
362
|
+
assert.match(rendered[0], /Rarebit Settings/);
|
|
363
|
+
assert.match(rendered[0], /Show Summary triggered/);
|
|
364
|
+
const parsed = JSON.parse(await readFile(box.settingsFile, "utf8"));
|
|
365
|
+
assert.equal(parsed.rarebit?.diagnostics?.summary_triggered, true);
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
test("the Rarebit palette is a human-only TUI surface and includes settings and Recap", async () => {
|
|
369
|
+
const box = await fixture({});
|
|
370
|
+
let modelCalls = 0;
|
|
371
|
+
let sessionWrites = 0;
|
|
372
|
+
let rendered = "";
|
|
373
|
+
const ui = {
|
|
374
|
+
custom: async (factory) => {
|
|
375
|
+
let selected;
|
|
376
|
+
const component = factory({ requestRender() {} }, theme(), {}, (value) => { selected = value; });
|
|
377
|
+
rendered = component.render(120).join("\n");
|
|
378
|
+
component.handleInput("\x1b");
|
|
379
|
+
return selected;
|
|
380
|
+
},
|
|
381
|
+
notify: () => {},
|
|
382
|
+
};
|
|
383
|
+
const selection = await openRarebitPalette(context(box, ui, {
|
|
384
|
+
sendUserMessage: () => { modelCalls += 1; },
|
|
385
|
+
sessionManager: { appendMessage: () => { sessionWrites += 1; } },
|
|
386
|
+
}));
|
|
387
|
+
assert.equal(selection, undefined);
|
|
388
|
+
assert.match(rendered, /Rarebit/);
|
|
389
|
+
assert.match(rendered, /Summary/);
|
|
390
|
+
assert.match(rendered, /Recap/);
|
|
391
|
+
assert.match(rendered, /Settings/);
|
|
392
|
+
assert.equal(modelCalls, 0);
|
|
393
|
+
assert.equal(sessionWrites, 0);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
test("/rarebit keeps menu and settings under one command surface", () => {
|
|
397
|
+
assert.deepEqual(parseRarebitCommand(""), { ok: true, subcommand: "menu", arguments: [] });
|
|
398
|
+
assert.deepEqual(parseRarebitCommand("settings"), { ok: true, subcommand: "settings", arguments: [] });
|
|
399
|
+
assert.deepEqual(parseRarebitCommand("settings project"), { ok: true, subcommand: "settings", arguments: ["project"] });
|
|
400
|
+
});
|
|
@@ -328,6 +328,26 @@ test("compact exact and owner-request prefix applicability re-derives native man
|
|
|
328
328
|
};
|
|
329
329
|
assert.equal(exactSelectionApplies(exact, current), true);
|
|
330
330
|
});
|
|
331
|
+
test("store promotes a newer manual assessment after an older settled receipt", async () => {
|
|
332
|
+
const f = await fixture();
|
|
333
|
+
const branch = [entry("u", "user", "Synthetic request"), entry("a", "assistant", "Synthetic completion")];
|
|
334
|
+
await writeFile(f.sessionFile, [JSON.stringify({ type: "session", id: "s" }), ...branch.map(JSON.stringify)].join("\n") + "\n");
|
|
335
|
+
const ctx = { sessionManager: { getHeader: () => ({ id: "s" }), getSessionFile: () => f.sessionFile, getBranch: () => branch } };
|
|
336
|
+
const config = {
|
|
337
|
+
sessionRoot: f.sessionRoot, rarebitRoot: f.rarebitRoot,
|
|
338
|
+
summaryPolicy: { minTotalLength: 0, maxRarebitRatio: 1 },
|
|
339
|
+
model: { provider: "test", id: "model" },
|
|
340
|
+
modelClient: { complete: async () => JSON.stringify({ summary: "Synthetic current assessment", sessionStatus: "finished", statusReason: "all_requests_accomplished" }) },
|
|
341
|
+
};
|
|
342
|
+
const settled = await processRarebitSummary(ctx, { ...config, lifecycleBoundary: "agent_settled" });
|
|
343
|
+
const manual = await processRarebitSummary(ctx, { ...config, lifecycleBoundary: "manual", forceSynthesis: true });
|
|
344
|
+
assert.notEqual(settled.record.jobId, manual.record.jobId);
|
|
345
|
+
const current = await readRarebitCurrent({ sessionFile: f.sessionFile, sessionRoot: f.sessionRoot, rarebitRoot: f.rarebitRoot });
|
|
346
|
+
assert.equal(current.artifactState.syncState, "assessment_current");
|
|
347
|
+
assert.equal(current.artifactState.receiptRef.jobId, manual.record.jobId);
|
|
348
|
+
assert.equal(current.receipt.lifecycleBoundary, "manual");
|
|
349
|
+
});
|
|
350
|
+
|
|
331
351
|
test("generated successful and terminal failure receipts satisfy runtime and JSON Schema closure", async () => {
|
|
332
352
|
const f = await fixture();
|
|
333
353
|
const success = await materialize(f, [entry("u", "user", "x")]);
|