@inetafrica/open-claudia 2.15.0 → 3.0.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/.env.example +30 -4
- package/CHANGELOG.md +22 -0
- package/README.md +87 -66
- package/bin/agent.js +44 -18
- package/bin/cli.js +30 -28
- package/bin/dream.js +5 -11
- package/bin/loopback-client.js +29 -5
- package/bin/schedule.js +19 -5
- package/bin/tool.js +23 -5
- package/bot-agent.js +5 -2350
- package/bot.js +81 -3
- package/channels/telegram/adapter.js +65 -5
- package/channels/voice/adapter.js +1 -0
- package/channels/voice/manage.js +43 -5
- package/config-dir.js +3 -11
- package/core/actions.js +155 -40
- package/core/approvals.js +136 -29
- package/core/auth-flow.js +103 -19
- package/core/config-dir.js +19 -0
- package/core/config.js +115 -69
- package/core/doctor.js +111 -57
- package/core/dream.js +219 -62
- package/core/enforcer.js +0 -0
- package/core/entities.js +2 -1
- package/core/fsutil.js +21 -4
- package/core/handlers.js +542 -224
- package/core/ideas.js +2 -1
- package/core/jobs.js +589 -72
- package/core/lessons.js +3 -2
- package/core/loopback.js +42 -6
- package/core/memory-mutation-queue.js +241 -0
- package/core/migration-backup.js +1060 -0
- package/core/pack-review.js +295 -88
- package/core/packs.js +4 -3
- package/core/persona.js +2 -1
- package/core/process-tree.js +19 -2
- package/core/provider-migration.js +39 -0
- package/core/provider-status.js +80 -0
- package/core/providers/claude-events.js +121 -0
- package/core/providers/claude-hook.js +114 -0
- package/core/providers/claude.js +296 -0
- package/core/providers/codex-events.js +125 -0
- package/core/providers/codex-hook.js +280 -0
- package/core/providers/codex.js +286 -0
- package/core/providers/contract.js +153 -0
- package/core/providers/env.js +148 -0
- package/core/providers/events.js +171 -0
- package/core/providers/hook-command.js +22 -0
- package/core/providers/index.js +240 -0
- package/core/providers/utility-policy.js +269 -0
- package/core/recall/classic.js +2 -2
- package/core/recall/discoverer.js +71 -22
- package/core/recall/metrics.js +27 -1
- package/core/recall/tuning.js +4 -1
- package/core/recall/warm-walker.js +151 -108
- package/core/recall-filter.js +86 -61
- package/core/router.js +79 -7
- package/core/run-context.js +185 -0
- package/core/runner.js +1562 -1286
- package/core/scheduler.js +515 -210
- package/core/side-chat.js +346 -0
- package/core/single-instance.js +46 -0
- package/core/state.js +1084 -97
- package/core/subagent.js +72 -98
- package/core/system-prompt.js +462 -279
- package/core/tool-guard.js +73 -39
- package/core/tools.js +22 -5
- package/core/transcripts.js +30 -1
- package/core/usage-log.js +19 -4
- package/core/utility-agent.js +654 -0
- package/core/web-auth.js +29 -13
- package/docs/CHANNEL_DESIGN.md +181 -0
- package/docs/MULTI_CHANNEL_PLAN.md +254 -0
- package/docs/PROVIDER_MIGRATION.md +67 -0
- package/health.js +50 -39
- package/package.json +48 -4
- package/setup.js +198 -38
- package/soul.md +39 -0
- package/test-ability-extraction.js +5 -0
- package/test-approval-async.js +63 -4
- package/test-cursor-removal.js +337 -0
- package/test-enforcer.js +70 -27
- package/test-fixtures/current-provider-parity.json +132 -0
- package/test-fixtures/fake-agent-cli.js +509 -0
- package/test-fixtures/migrations/claude-only/jobs.json +3 -0
- package/test-fixtures/migrations/claude-only/sessions.json +5 -0
- package/test-fixtures/migrations/claude-only/state.json +5 -0
- package/test-fixtures/migrations/cursor-selected/crons.json +3 -0
- package/test-fixtures/migrations/cursor-selected/jobs.json +3 -0
- package/test-fixtures/migrations/cursor-selected/sessions.json +5 -0
- package/test-fixtures/migrations/cursor-selected/state.json +6 -0
- package/test-fixtures/migrations/dual-provider/jobs.json +3 -0
- package/test-fixtures/migrations/dual-provider/sessions.json +7 -0
- package/test-fixtures/migrations/dual-provider/state.json +10 -0
- package/test-fixtures/migrations/malformed-partial/jobs.json +1 -0
- package/test-fixtures/migrations/malformed-partial/sessions.json +1 -0
- package/test-fixtures/migrations/malformed-partial/sessions.json.bak +3 -0
- package/test-fixtures/migrations/malformed-partial/state.json +1 -0
- package/test-fixtures/migrations/malformed-partial/state.json.bak +5 -0
- package/test-fixtures/migrations/missing-project/jobs.json +3 -0
- package/test-fixtures/migrations/missing-project/sessions.json +3 -0
- package/test-fixtures/migrations/missing-project/state.json +4 -0
- package/test-fixtures/migrations/multi-user/jobs.json +4 -0
- package/test-fixtures/migrations/multi-user/sessions.json +4 -0
- package/test-fixtures/migrations/multi-user/state.json +6 -0
- package/test-fixtures/provider-event-samples.json +17 -0
- package/test-learning-e2e.js +5 -0
- package/test-memory-mutation-queue.js +191 -0
- package/test-provider-boot-matrix.js +399 -0
- package/test-provider-bootstrap.js +158 -0
- package/test-provider-capabilities.js +232 -0
- package/test-provider-claude.js +206 -0
- package/test-provider-codex.js +228 -0
- package/test-provider-compaction.js +371 -0
- package/test-provider-core-prompt.js +264 -0
- package/test-provider-coupling-audit.js +90 -0
- package/test-provider-dream.js +312 -0
- package/test-provider-enforcer.js +252 -0
- package/test-provider-env-isolation.js +150 -0
- package/test-provider-events.js +171 -0
- package/test-provider-fixture.js +332 -0
- package/test-provider-gateway.js +508 -0
- package/test-provider-language.js +89 -0
- package/test-provider-migration-backup.js +424 -0
- package/test-provider-pack-review.js +436 -0
- package/test-provider-parity-e2e.js +537 -0
- package/test-provider-prompt-parity.js +89 -0
- package/test-provider-recall.js +271 -0
- package/test-provider-registry.js +251 -0
- package/test-provider-resume-parity.js +41 -0
- package/test-provider-run-lifecycle.js +349 -0
- package/test-provider-runner.js +271 -0
- package/test-provider-scheduler.js +689 -0
- package/test-provider-session-commands.js +185 -0
- package/test-provider-session-history.js +205 -0
- package/test-provider-side-chat.js +337 -0
- package/test-provider-state-migration.js +316 -0
- package/test-provider-stream-decoder.js +69 -0
- package/test-provider-subagent.js +228 -0
- package/test-provider-tool-hooks.js +360 -0
- package/test-recall-discoverer.js +1 -0
- package/test-recall-engine.js +3 -3
- package/test-recall-evolution.js +18 -0
- package/test-runner-watchdog-static.js +16 -8
- package/test-single-runtime.js +35 -0
- package/test-telegram-poll-recovery.js +93 -0
- package/test-tool-guard.js +56 -0
- package/test-tools.js +19 -0
- package/test-unified-identity.js +3 -1
- package/test-usage-accounting.js +92 -20
- package/test-utility-provider-policy.js +486 -0
- package/web.js +130 -35
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
"use strict";
|
|
4
|
+
|
|
5
|
+
const assert = require("assert");
|
|
6
|
+
const fs = require("fs");
|
|
7
|
+
const os = require("os");
|
|
8
|
+
const path = require("path");
|
|
9
|
+
const { spawnSync } = require("child_process");
|
|
10
|
+
|
|
11
|
+
const root = __dirname;
|
|
12
|
+
const configDir = fs.mkdtempSync(path.join(os.tmpdir(), "open-claudia-compaction-"));
|
|
13
|
+
const workspace = path.join(configDir, "workspace");
|
|
14
|
+
const alphaDir = path.join(workspace, "alpha");
|
|
15
|
+
const betaDir = path.join(workspace, "beta");
|
|
16
|
+
fs.mkdirSync(alphaDir, { recursive: true });
|
|
17
|
+
fs.mkdirSync(betaDir, { recursive: true });
|
|
18
|
+
process.env.OPEN_CLAUDIA_CONFIG_DIR = configDir;
|
|
19
|
+
process.env.WORKSPACE = workspace;
|
|
20
|
+
process.env.TELEGRAM_CHAT_ID = "fixture";
|
|
21
|
+
process.env.OPEN_CLAUDIA_TEST = "1";
|
|
22
|
+
process.env.CLAUDE_PATH = process.execPath;
|
|
23
|
+
process.env.CODEX_PATH = process.execPath;
|
|
24
|
+
|
|
25
|
+
const stateApi = require("./core/state");
|
|
26
|
+
const runner = require("./core/runner");
|
|
27
|
+
|
|
28
|
+
const state = stateApi.currentState();
|
|
29
|
+
|
|
30
|
+
function result(overrides = {}) {
|
|
31
|
+
return {
|
|
32
|
+
ok: true,
|
|
33
|
+
status: "succeeded",
|
|
34
|
+
terminalSeen: true,
|
|
35
|
+
text: "Summary\n\n=== CONDENSED SEED ===\nA sufficiently detailed condensed continuation that is longer than eighty characters for the fixture to accept.",
|
|
36
|
+
sessionId: "new-session",
|
|
37
|
+
usage: { input_tokens: 40, cached_input_tokens: 10, output_tokens: 4 },
|
|
38
|
+
usageScope: "session_total",
|
|
39
|
+
...overrides,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function resetFixture(provider = "claude") {
|
|
44
|
+
state.currentSession = { name: "alpha", dir: alphaDir };
|
|
45
|
+
state.settings.backend = provider;
|
|
46
|
+
state.activeSessions = {
|
|
47
|
+
alpha: { claude: "old-claude", codex: "old-codex" },
|
|
48
|
+
beta: { claude: "beta-claude", codex: "beta-codex" },
|
|
49
|
+
};
|
|
50
|
+
state.usageBySession = {
|
|
51
|
+
[`alpha\0${provider}\0old-${provider}`]: {
|
|
52
|
+
...stateApi.freshUsage(),
|
|
53
|
+
turns: 7,
|
|
54
|
+
inputTokens: 700,
|
|
55
|
+
liveContextTokens: 900,
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
state.pendingCompaction = null;
|
|
59
|
+
state.completedCompactions = {};
|
|
60
|
+
state.lastCompactedAt = 0;
|
|
61
|
+
state.isCompacting = false;
|
|
62
|
+
state.runningProcess = null;
|
|
63
|
+
state.preparingRun = false;
|
|
64
|
+
state.isFirstMessage = false;
|
|
65
|
+
stateApi.replaceProjectSessions(state.userId, "alpha", [
|
|
66
|
+
{
|
|
67
|
+
id: `old-${provider}`,
|
|
68
|
+
provider,
|
|
69
|
+
project: "alpha",
|
|
70
|
+
title: "Old session",
|
|
71
|
+
created: "2026-01-01T00:00:00.000Z",
|
|
72
|
+
lastUsed: "2026-01-01T00:00:00.000Z",
|
|
73
|
+
selectable: true,
|
|
74
|
+
},
|
|
75
|
+
], { strict: true });
|
|
76
|
+
stateApi.saveState({ strict: true });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function serviceWith(runCapture, extra = {}) {
|
|
80
|
+
return runner.createCompactionService({
|
|
81
|
+
getState: () => state,
|
|
82
|
+
captureRunContext: runner.admitRunContext,
|
|
83
|
+
runCapture,
|
|
84
|
+
commitCompaction: stateApi.commitProviderCompaction,
|
|
85
|
+
archiveBrief: () => null,
|
|
86
|
+
appendDaySeed: () => {},
|
|
87
|
+
collectRepoFacts: () => null,
|
|
88
|
+
collectRecentTurns: () => null,
|
|
89
|
+
...extra,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function immutableSuccessProbe() {
|
|
94
|
+
resetFixture("claude");
|
|
95
|
+
state.isCompacting = true;
|
|
96
|
+
assert.strictEqual(stateApi.acquireRunLock(state), false, "foreground admission stays closed between compaction stages");
|
|
97
|
+
state.isCompacting = false;
|
|
98
|
+
const contexts = [];
|
|
99
|
+
const service = serviceWith(async (_prompt, _cwd, options) => {
|
|
100
|
+
assert.strictEqual(options.persist, false, "compaction provider stages have no ordinary capture persistence");
|
|
101
|
+
contexts.push(options.runContext);
|
|
102
|
+
assert.strictEqual(stateApi.getProviderSession(state, "claude", "alpha"), "old-claude", "pointer stays old during both provider stages");
|
|
103
|
+
if (contexts.length === 1) {
|
|
104
|
+
state.currentSession = { name: "beta", dir: betaDir };
|
|
105
|
+
state.settings.backend = "codex";
|
|
106
|
+
state.isFirstMessage = true;
|
|
107
|
+
return result({ sessionId: "old-claude" });
|
|
108
|
+
}
|
|
109
|
+
return result({ sessionId: "new-claude" });
|
|
110
|
+
});
|
|
111
|
+
const compacted = await service.compact(alphaDir, { operationId: "compact-success" });
|
|
112
|
+
assert.strictEqual(compacted.compacted, true);
|
|
113
|
+
assert.strictEqual(contexts.length, 2);
|
|
114
|
+
assert.strictEqual(contexts[0].provider, "claude");
|
|
115
|
+
assert.strictEqual(contexts[0].project.name, "alpha");
|
|
116
|
+
assert.strictEqual(contexts[0].sessionId, "old-claude");
|
|
117
|
+
assert.ok(Number.isFinite(contexts[0].timeoutMs) && contexts[0].timeoutMs > 0);
|
|
118
|
+
assert.strictEqual(contexts[1].provider, "claude");
|
|
119
|
+
assert.strictEqual(contexts[1].project.name, "alpha");
|
|
120
|
+
assert.strictEqual(contexts[1].fresh, true);
|
|
121
|
+
assert.strictEqual(contexts[1].readOnly, true);
|
|
122
|
+
assert.strictEqual(contexts[1].permissionMode, "plan");
|
|
123
|
+
assert.strictEqual(contexts[1].maxTurns, 1);
|
|
124
|
+
assert.strictEqual(contexts[1].timeoutMs, contexts[0].timeoutMs);
|
|
125
|
+
const restrictedEnv = runner.decorateForegroundInvocation(
|
|
126
|
+
{ env: { FIXTURE_SAFE: "1" } },
|
|
127
|
+
contexts[0],
|
|
128
|
+
{ transcriptPaths: null },
|
|
129
|
+
).env;
|
|
130
|
+
assert.strictEqual(restrictedEnv.OC_PROVIDER, "claude");
|
|
131
|
+
assert.ok(!Object.prototype.hasOwnProperty.call(restrictedEnv, "OC_SEND_URL"));
|
|
132
|
+
assert.ok(!Object.prototype.hasOwnProperty.call(restrictedEnv, "OC_CANONICAL_USER_ID"));
|
|
133
|
+
assert.ok(!Object.prototype.hasOwnProperty.call(restrictedEnv, "OC_LAST_SESSION_ID"));
|
|
134
|
+
assert.strictEqual(state.currentSession.name, "beta", "live project switch is preserved");
|
|
135
|
+
assert.strictEqual(state.settings.backend, "codex", "live provider switch is preserved");
|
|
136
|
+
assert.strictEqual(state.isFirstMessage, true, "the switched live tuple keeps its transient first-message state");
|
|
137
|
+
assert.strictEqual(stateApi.getProviderSession(state, "claude", "alpha"), "new-claude");
|
|
138
|
+
assert.strictEqual(stateApi.getProviderSession(state, "codex", "beta"), "beta-codex");
|
|
139
|
+
const history = stateApi.getProjectSessions(state.userId, "alpha", "claude");
|
|
140
|
+
assert.strictEqual(history.filter((entry) => entry.id === "new-claude").length, 1);
|
|
141
|
+
assert.deepStrictEqual(history[0].lineage, {
|
|
142
|
+
type: "compaction",
|
|
143
|
+
operationId: "compact-success",
|
|
144
|
+
previousSessionId: "old-claude",
|
|
145
|
+
newSessionId: "new-claude",
|
|
146
|
+
});
|
|
147
|
+
const usage = stateApi.getUsageForSession(state, "alpha", "claude", "new-claude");
|
|
148
|
+
assert.strictEqual(usage.turns, 0, "seed is not billed as a user turn");
|
|
149
|
+
assert.strictEqual(usage.compactionCheckpoint.previousSessionId, "old-claude");
|
|
150
|
+
assert.strictEqual(state.isCompacting, false);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function failureProbe() {
|
|
154
|
+
resetFixture("claude");
|
|
155
|
+
state.runningProcess = { pid: 123 };
|
|
156
|
+
let busyCalls = 0;
|
|
157
|
+
const busy = await serviceWith(async () => { busyCalls += 1; return result(); }).compact(alphaDir);
|
|
158
|
+
assert.strictEqual(busy.compacted, false);
|
|
159
|
+
assert.strictEqual(busyCalls, 0, "manual compaction never overlaps an active provider run");
|
|
160
|
+
state.runningProcess = null;
|
|
161
|
+
|
|
162
|
+
resetFixture("claude");
|
|
163
|
+
let summaryCalls = 0;
|
|
164
|
+
const summaryFailure = serviceWith(async () => {
|
|
165
|
+
summaryCalls += 1;
|
|
166
|
+
return result({ ok: false, status: "failed", sessionId: "old-claude" });
|
|
167
|
+
});
|
|
168
|
+
const failedSummary = await summaryFailure.compact(alphaDir, { operationId: "compact-summary-failed" });
|
|
169
|
+
assert.strictEqual(failedSummary.compacted, false);
|
|
170
|
+
assert.strictEqual(summaryCalls, 1, "a failed summary never starts a seed");
|
|
171
|
+
assert.strictEqual(stateApi.getProviderSession(state, "claude", "alpha"), "old-claude");
|
|
172
|
+
|
|
173
|
+
for (const [label, seed] of [
|
|
174
|
+
["failed", result({ ok: false, status: "failed", sessionId: "failed-new" })],
|
|
175
|
+
["nonterminal", result({ terminalSeen: false, sessionId: "nonterminal-new" })],
|
|
176
|
+
["missing-session", result({ sessionId: null })],
|
|
177
|
+
]) {
|
|
178
|
+
resetFixture("claude");
|
|
179
|
+
let calls = 0;
|
|
180
|
+
const service = serviceWith(async () => (++calls === 1 ? result({ sessionId: "old-claude" }) : seed));
|
|
181
|
+
const compacted = await service.compact(alphaDir, { operationId: `compact-${label}` });
|
|
182
|
+
assert.strictEqual(compacted.compacted, false, label);
|
|
183
|
+
assert.strictEqual(stateApi.getProviderSession(state, "claude", "alpha"), "old-claude", label);
|
|
184
|
+
assert.strictEqual(stateApi.getProjectSessions(state.userId, "alpha", "claude").length, 1, label);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
resetFixture("claude");
|
|
188
|
+
let calls = 0;
|
|
189
|
+
let stateWrites = 0;
|
|
190
|
+
const persistenceFailure = serviceWith(
|
|
191
|
+
async () => (++calls === 1 ? result({ sessionId: "old-claude" }) : result({ sessionId: "persist-failed-new" })),
|
|
192
|
+
{
|
|
193
|
+
commitCompaction: (target, transaction) => stateApi.commitProviderCompaction(target, transaction, {
|
|
194
|
+
persistState(options) {
|
|
195
|
+
stateWrites += 1;
|
|
196
|
+
if (stateWrites === 2) throw Object.assign(new Error("fixture persistence failure"), { code: "FIXTURE_PERSISTENCE" });
|
|
197
|
+
return stateApi.saveState(options);
|
|
198
|
+
},
|
|
199
|
+
}),
|
|
200
|
+
},
|
|
201
|
+
);
|
|
202
|
+
const failed = await persistenceFailure.compact(alphaDir, { operationId: "compact-persist-failure" });
|
|
203
|
+
assert.strictEqual(failed.compacted, false);
|
|
204
|
+
assert.strictEqual(stateApi.getProviderSession(state, "claude", "alpha"), "old-claude");
|
|
205
|
+
assert.strictEqual(stateApi.getProjectSessions(state.userId, "alpha", "claude").length, 1);
|
|
206
|
+
|
|
207
|
+
resetFixture("claude");
|
|
208
|
+
calls = 0;
|
|
209
|
+
const rejectedHistory = serviceWith(
|
|
210
|
+
async () => (++calls === 1 ? result({ sessionId: "old-claude" }) : result({ sessionId: "history-rejected-new" })),
|
|
211
|
+
{
|
|
212
|
+
commitCompaction: (target, transaction) => stateApi.commitProviderCompaction(target, transaction, {
|
|
213
|
+
recordHistory: () => false,
|
|
214
|
+
}),
|
|
215
|
+
},
|
|
216
|
+
);
|
|
217
|
+
const historyFailed = await rejectedHistory.compact(alphaDir, { operationId: "compact-history-rejected" });
|
|
218
|
+
assert.strictEqual(historyFailed.compacted, false);
|
|
219
|
+
assert.strictEqual(stateApi.getProviderSession(state, "claude", "alpha"), "old-claude");
|
|
220
|
+
assert.deepStrictEqual(stateApi.getProjectSessions(state.userId, "alpha", "claude").map((entry) => entry.id), ["old-claude"]);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async function codexRestrictionProbe() {
|
|
224
|
+
resetFixture("codex");
|
|
225
|
+
const contexts = [];
|
|
226
|
+
const service = serviceWith(async (_prompt, _cwd, options) => {
|
|
227
|
+
contexts.push(options.runContext);
|
|
228
|
+
return contexts.length === 1
|
|
229
|
+
? result({ sessionId: "old-codex" })
|
|
230
|
+
: result({ sessionId: "new-codex", usage: { input_tokens: 90, cached_input_tokens: 50, output_tokens: 5 } });
|
|
231
|
+
});
|
|
232
|
+
const compacted = await service.compact(alphaDir, { operationId: "compact-codex" });
|
|
233
|
+
assert.strictEqual(compacted.compacted, true);
|
|
234
|
+
const seed = contexts[1];
|
|
235
|
+
assert.strictEqual(seed.provider, "codex");
|
|
236
|
+
assert.strictEqual(seed.readOnly, true);
|
|
237
|
+
assert.strictEqual(seed.permissionMode, "plan");
|
|
238
|
+
assert.strictEqual(seed.maxTurns, null, "Codex maxTurns is honestly unsupported");
|
|
239
|
+
assert.ok(seed.outputSchemaPath && fs.existsSync(seed.outputSchemaPath));
|
|
240
|
+
const usage = stateApi.getUsageForSession(state, "alpha", "codex", "new-codex");
|
|
241
|
+
assert.deepStrictEqual(usage.providerUsageSnapshot, {
|
|
242
|
+
input_tokens: 90,
|
|
243
|
+
cached_input_tokens: 50,
|
|
244
|
+
output_tokens: 5,
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
const codexProvider = require("./core/providers/codex").createCodexProvider({
|
|
248
|
+
hookTransport: false,
|
|
249
|
+
resolveExecutable: () => process.execPath,
|
|
250
|
+
buildEnv: () => ({}),
|
|
251
|
+
getAuthStatus: () => ({ state: "authenticated", reason: null }),
|
|
252
|
+
});
|
|
253
|
+
const codexInvocation = codexProvider.buildCompactionInvocation({
|
|
254
|
+
coreInstructions: "fixture policy",
|
|
255
|
+
userPrompt: "acknowledge",
|
|
256
|
+
providerSettings: { permissionMode: "plan", budget: null, worktree: false },
|
|
257
|
+
fresh: true,
|
|
258
|
+
readOnly: true,
|
|
259
|
+
maxTurns: null,
|
|
260
|
+
outputSchemaPath: seed.outputSchemaPath,
|
|
261
|
+
imagePaths: [],
|
|
262
|
+
});
|
|
263
|
+
assert.ok(codexInvocation.args.includes("read-only"));
|
|
264
|
+
assert.ok(codexInvocation.args.includes("--output-schema"));
|
|
265
|
+
assert.ok(!codexInvocation.args.includes("--ephemeral"), "persisted Codex seed is resumable");
|
|
266
|
+
assert.ok(!codexInvocation.args.includes("--dangerously-bypass-approvals-and-sandbox"));
|
|
267
|
+
|
|
268
|
+
const claudeProvider = require("./core/providers/claude").createClaudeProvider({
|
|
269
|
+
hookTransport: false,
|
|
270
|
+
resolveExecutable: () => process.execPath,
|
|
271
|
+
buildEnv: () => ({}),
|
|
272
|
+
getAuthStatus: () => ({ state: "authenticated", reason: null }),
|
|
273
|
+
});
|
|
274
|
+
const claudeInvocation = claudeProvider.buildCompactionInvocation({
|
|
275
|
+
coreInstructions: "fixture policy",
|
|
276
|
+
userPrompt: "acknowledge",
|
|
277
|
+
providerSettings: { permissionMode: "plan", budget: null, worktree: false },
|
|
278
|
+
fresh: true,
|
|
279
|
+
readOnly: true,
|
|
280
|
+
maxTurns: 1,
|
|
281
|
+
imagePaths: [],
|
|
282
|
+
});
|
|
283
|
+
assert.deepStrictEqual(claudeInvocation.args.slice(claudeInvocation.args.indexOf("--max-turns"), claudeInvocation.args.indexOf("--max-turns") + 2), ["--max-turns", "1"]);
|
|
284
|
+
assert.deepStrictEqual(claudeInvocation.args.slice(claudeInvocation.args.indexOf("--permission-mode"), claudeInvocation.args.indexOf("--permission-mode") + 2), ["--permission-mode", "plan"]);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function duplicateCommitProbe() {
|
|
288
|
+
resetFixture("claude");
|
|
289
|
+
const transaction = {
|
|
290
|
+
operationId: "compact-duplicate",
|
|
291
|
+
canonicalUserId: state.userId,
|
|
292
|
+
project: "alpha",
|
|
293
|
+
provider: "claude",
|
|
294
|
+
previousSessionId: "old-claude",
|
|
295
|
+
newSessionId: "duplicate-new",
|
|
296
|
+
title: "Compacted fixture",
|
|
297
|
+
seedUsage: null,
|
|
298
|
+
};
|
|
299
|
+
const first = stateApi.commitProviderCompaction(state, transaction);
|
|
300
|
+
const second = stateApi.commitProviderCompaction(state, transaction);
|
|
301
|
+
assert.strictEqual(first.committed, true);
|
|
302
|
+
assert.strictEqual(second.duplicate, true);
|
|
303
|
+
assert.strictEqual(stateApi.getProjectSessions(state.userId, "alpha", "claude").filter((entry) => entry.id === "duplicate-new").length, 1);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function restartRecoveryProbe() {
|
|
307
|
+
resetFixture("claude");
|
|
308
|
+
const childScript = [
|
|
309
|
+
"const stateApi = require('./core/state');",
|
|
310
|
+
"const state = stateApi.currentState();",
|
|
311
|
+
"stateApi.commitProviderCompaction(state, {",
|
|
312
|
+
" operationId: 'compact-crash', canonicalUserId: state.userId, project: 'alpha', provider: 'claude',",
|
|
313
|
+
" previousSessionId: 'old-claude', newSessionId: 'crash-new', title: 'Crash fixture', seedUsage: null,",
|
|
314
|
+
"}, { faultInjector(point) { if (point === 'after-compaction-history') process.exit(73); } });",
|
|
315
|
+
].join("\n");
|
|
316
|
+
const crashed = spawnSync(process.execPath, ["-e", childScript], { cwd: root, env: process.env, encoding: "utf8" });
|
|
317
|
+
assert.strictEqual(crashed.status, 73, crashed.stderr || crashed.stdout);
|
|
318
|
+
|
|
319
|
+
const recovered = spawnSync(process.execPath, ["-e", [
|
|
320
|
+
"const s = require('./core/state');",
|
|
321
|
+
"const state = s.currentState();",
|
|
322
|
+
"process.stdout.write(JSON.stringify({ pointer: s.getProviderSession(state, 'claude', 'alpha'), history: s.getProjectSessions(state.userId, 'alpha', 'claude').map(x => x.id), pending: state.pendingCompaction || null }));",
|
|
323
|
+
].join("\n")], { cwd: root, env: process.env, encoding: "utf8" });
|
|
324
|
+
assert.strictEqual(recovered.status, 0, recovered.stderr || recovered.stdout);
|
|
325
|
+
assert.deepStrictEqual(JSON.parse(recovered.stdout), {
|
|
326
|
+
pointer: "old-claude",
|
|
327
|
+
history: ["old-claude"],
|
|
328
|
+
pending: null,
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
resetFixture("claude");
|
|
332
|
+
const committedCrashScript = [
|
|
333
|
+
"const stateApi = require('./core/state');",
|
|
334
|
+
"const state = stateApi.currentState();",
|
|
335
|
+
"stateApi.commitProviderCompaction(state, {",
|
|
336
|
+
" operationId: 'compact-committed-crash', canonicalUserId: state.userId, project: 'alpha', provider: 'claude',",
|
|
337
|
+
" previousSessionId: 'old-claude', newSessionId: 'committed-crash-new', title: 'Committed crash fixture', seedUsage: null,",
|
|
338
|
+
"}, { faultInjector(point) { if (point === 'after-compaction-state-commit') process.exit(74); } });",
|
|
339
|
+
].join("\n");
|
|
340
|
+
const committedCrash = spawnSync(process.execPath, ["-e", committedCrashScript], { cwd: root, env: process.env, encoding: "utf8" });
|
|
341
|
+
assert.strictEqual(committedCrash.status, 74, committedCrash.stderr || committedCrash.stdout);
|
|
342
|
+
const finalized = spawnSync(process.execPath, ["-e", [
|
|
343
|
+
"const s = require('./core/state');",
|
|
344
|
+
"const state = s.currentState();",
|
|
345
|
+
"process.stdout.write(JSON.stringify({ pointer: s.getProviderSession(state, 'claude', 'alpha'), history: s.getProjectSessions(state.userId, 'alpha', 'claude').map(x => x.id), pending: state.pendingCompaction || null }));",
|
|
346
|
+
].join("\n")], { cwd: root, env: process.env, encoding: "utf8" });
|
|
347
|
+
assert.strictEqual(finalized.status, 0, finalized.stderr || finalized.stdout);
|
|
348
|
+
assert.deepStrictEqual(JSON.parse(finalized.stdout), {
|
|
349
|
+
pointer: "committed-crash-new",
|
|
350
|
+
history: ["committed-crash-new", "old-claude"],
|
|
351
|
+
pending: null,
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
(async () => {
|
|
356
|
+
try {
|
|
357
|
+
assert.strictEqual(typeof runner.createCompactionService, "function");
|
|
358
|
+
assert.strictEqual(typeof stateApi.commitProviderCompaction, "function");
|
|
359
|
+
await immutableSuccessProbe();
|
|
360
|
+
await failureProbe();
|
|
361
|
+
await codexRestrictionProbe();
|
|
362
|
+
duplicateCommitProbe();
|
|
363
|
+
restartRecoveryProbe();
|
|
364
|
+
console.log("provider compaction OK");
|
|
365
|
+
} finally {
|
|
366
|
+
fs.rmSync(configDir, { recursive: true, force: true });
|
|
367
|
+
}
|
|
368
|
+
})().catch((error) => {
|
|
369
|
+
console.error(error);
|
|
370
|
+
process.exit(1);
|
|
371
|
+
});
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
"use strict";
|
|
4
|
+
|
|
5
|
+
const assert = require("assert");
|
|
6
|
+
const fs = require("fs");
|
|
7
|
+
const os = require("os");
|
|
8
|
+
const path = require("path");
|
|
9
|
+
const { spawnSync } = require("child_process");
|
|
10
|
+
|
|
11
|
+
async function probe() {
|
|
12
|
+
const {
|
|
13
|
+
PromptConstructionError,
|
|
14
|
+
buildTurnPrompt,
|
|
15
|
+
createPromptBuilder,
|
|
16
|
+
promptDedupeIdentity,
|
|
17
|
+
} = require("./core/system-prompt");
|
|
18
|
+
const { getProvider } = require("./core/providers");
|
|
19
|
+
const { runInChat } = require("./core/context");
|
|
20
|
+
const { currentState } = require("./core/state");
|
|
21
|
+
|
|
22
|
+
assert.strictEqual(typeof buildTurnPrompt, "function");
|
|
23
|
+
assert.strictEqual(typeof createPromptBuilder, "function");
|
|
24
|
+
assert.strictEqual(typeof promptDedupeIdentity, "function");
|
|
25
|
+
|
|
26
|
+
const adapter = {
|
|
27
|
+
id: "telegram-fixture",
|
|
28
|
+
type: "telegram",
|
|
29
|
+
send: async () => "fixture-message",
|
|
30
|
+
typing: async () => {},
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
await runInChat({
|
|
34
|
+
adapter,
|
|
35
|
+
channelId: "fixture-channel",
|
|
36
|
+
canonicalUserId: "fixture-user",
|
|
37
|
+
userId: "fixture-channel",
|
|
38
|
+
transport: "telegram",
|
|
39
|
+
raw: null,
|
|
40
|
+
}, async () => {
|
|
41
|
+
const state = currentState();
|
|
42
|
+
state.currentSession = { name: "fixture-project", dir: process.env.WORKSPACE };
|
|
43
|
+
state.settings = { ...state.settings, backend: "claude" };
|
|
44
|
+
state.activeSessions["fixture-project"] = { claude: "claude-session", codex: "codex-session" };
|
|
45
|
+
|
|
46
|
+
let recallCalls = 0;
|
|
47
|
+
const promptBuilder = createPromptBuilder({
|
|
48
|
+
async buildRecallContext() {
|
|
49
|
+
recallCalls += 1;
|
|
50
|
+
return {
|
|
51
|
+
dynamicContext: [
|
|
52
|
+
"## Active Open Claudia skills / context packs",
|
|
53
|
+
"PACK CONTEXT SENTINEL",
|
|
54
|
+
"## Known entities",
|
|
55
|
+
"ENTITY CONTEXT SENTINEL",
|
|
56
|
+
"## Tools that may help here",
|
|
57
|
+
"TOOL CONTEXT SENTINEL",
|
|
58
|
+
].join("\n"),
|
|
59
|
+
metadata: { status: "included", engine: "fixture", omitted: false },
|
|
60
|
+
};
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const common = {
|
|
65
|
+
canonicalUserId: "fixture-user",
|
|
66
|
+
project: { name: "fixture-project", dir: process.env.WORKSPACE },
|
|
67
|
+
providerSettings: { model: null, effort: null, permissionMode: null },
|
|
68
|
+
channelId: "fixture-channel",
|
|
69
|
+
origin: { transport: "telegram", channelId: "fixture-channel" },
|
|
70
|
+
};
|
|
71
|
+
const claudeContext = { ...common, provider: "claude", sessionId: "claude-session" };
|
|
72
|
+
const codexContext = { ...common, provider: "codex", sessionId: "codex-session" };
|
|
73
|
+
|
|
74
|
+
const claude = await promptBuilder.buildTurnPrompt("FIRST USER SENTINEL", {
|
|
75
|
+
runContext: claudeContext,
|
|
76
|
+
provider: getProvider("claude"),
|
|
77
|
+
});
|
|
78
|
+
const codex = await promptBuilder.buildTurnPrompt("SECOND USER SENTINEL", {
|
|
79
|
+
runContext: codexContext,
|
|
80
|
+
provider: getProvider("codex"),
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
for (const bundle of [claude, codex]) {
|
|
84
|
+
assert.strictEqual(typeof bundle.coreInstructions, "string");
|
|
85
|
+
assert.strictEqual(typeof bundle.dynamicContext, "string");
|
|
86
|
+
assert.ok(bundle.coreInstructions.includes("SOUL POLICY SENTINEL"), "soul is mandatory core policy");
|
|
87
|
+
assert.ok(bundle.coreInstructions.includes("PERSONA SENTINEL"), "persona is included in core policy");
|
|
88
|
+
assert.ok(bundle.coreInstructions.includes("LESSON SENTINEL"), "lessons are included in core policy");
|
|
89
|
+
assert.ok(bundle.coreInstructions.includes("## Delivery"), "delivery guidance is mandatory");
|
|
90
|
+
assert.ok(bundle.coreInstructions.includes("## Background work"), "scheduler guidance is mandatory");
|
|
91
|
+
assert.ok(bundle.coreInstructions.includes("Persistent todo list"), "task guidance is mandatory");
|
|
92
|
+
assert.ok(bundle.coreInstructions.includes("## Tools are"), "tool guidance is mandatory");
|
|
93
|
+
assert.ok(bundle.coreInstructions.includes("channel context is already in the env"), "gateway guidance is mandatory");
|
|
94
|
+
assert.ok(bundle.dynamicContext.includes("## Runtime state (current turn)"));
|
|
95
|
+
assert.ok(bundle.dynamicContext.includes("## Project Transcript Memory"));
|
|
96
|
+
assert.ok(bundle.dynamicContext.includes("## Speaker"));
|
|
97
|
+
assert.ok(bundle.dynamicContext.includes("PACK CONTEXT SENTINEL"));
|
|
98
|
+
assert.ok(bundle.dynamicContext.includes("ENTITY CONTEXT SENTINEL"));
|
|
99
|
+
assert.ok(bundle.dynamicContext.includes("TOOL CONTEXT SENTINEL"));
|
|
100
|
+
assert.deepStrictEqual(bundle.recallMetadata, { status: "included", engine: "fixture", omitted: false });
|
|
101
|
+
assert.ok(bundle.transcriptPaths && bundle.transcriptPaths.transcriptPath);
|
|
102
|
+
assert.ok(!bundle.coreInstructions.includes("Claude Code harness"));
|
|
103
|
+
assert.ok(!bundle.dynamicContext.includes("Claude Code harness"));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
assert.strictEqual(claude.coreInstructions, codex.coreInstructions, "provider-neutral core is byte-identical");
|
|
107
|
+
assert.notStrictEqual(claude.userPrompt, codex.userPrompt, "the user prompt remains per-turn data");
|
|
108
|
+
assert.ok(claude.dynamicContext.includes(getProvider("claude").nativeToolsNote));
|
|
109
|
+
assert.ok(codex.dynamicContext.includes(getProvider("codex").nativeToolsNote));
|
|
110
|
+
assert.ok(!claude.dynamicContext.includes(getProvider("codex").nativeToolsNote));
|
|
111
|
+
assert.strictEqual(recallCalls, 2);
|
|
112
|
+
|
|
113
|
+
const again = await promptBuilder.buildTurnPrompt("THIRD USER SENTINEL", {
|
|
114
|
+
runContext: claudeContext,
|
|
115
|
+
provider: getProvider("claude"),
|
|
116
|
+
});
|
|
117
|
+
assert.strictEqual(again.coreInstructions, claude.coreInstructions, "core instructions stay byte-stable within a session");
|
|
118
|
+
|
|
119
|
+
const identityA = promptDedupeIdentity({ runContext: claudeContext });
|
|
120
|
+
const identityB = promptDedupeIdentity({ runContext: { ...claudeContext, sessionId: "next-session" } });
|
|
121
|
+
const identityC = promptDedupeIdentity({ runContext: { ...claudeContext, provider: "codex" } });
|
|
122
|
+
const identityD = promptDedupeIdentity({ runContext: { ...claudeContext, project: { name: "other", dir: "/other" } } });
|
|
123
|
+
assert.notStrictEqual(identityA, identityB, "session participates in dedupe identity");
|
|
124
|
+
assert.notStrictEqual(identityA, identityC, "provider participates in dedupe identity");
|
|
125
|
+
assert.notStrictEqual(identityA, identityD, "project participates in dedupe identity");
|
|
126
|
+
assert.notStrictEqual(
|
|
127
|
+
promptDedupeIdentity({ runContext: { ...claudeContext, sessionId: null, runId: "new-run-1" } }),
|
|
128
|
+
promptDedupeIdentity({ runContext: { ...claudeContext, sessionId: null, runId: "new-run-2" } }),
|
|
129
|
+
"separate admitted runs cannot share the pre-session dedupe bucket",
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
const brokenMandatory = createPromptBuilder({
|
|
133
|
+
buildCoreInstructions() { throw new Error("fixture mandatory failure"); },
|
|
134
|
+
});
|
|
135
|
+
await assert.rejects(
|
|
136
|
+
() => brokenMandatory.buildTurnPrompt("RAW MUST NOT ESCAPE", {
|
|
137
|
+
runContext: claudeContext,
|
|
138
|
+
provider: getProvider("claude"),
|
|
139
|
+
}),
|
|
140
|
+
(error) => error instanceof PromptConstructionError
|
|
141
|
+
&& error.code === "PROMPT_CONSTRUCTION_FAILED"
|
|
142
|
+
&& error.stage === "coreInstructions"
|
|
143
|
+
&& !String(error.bundle || "").includes("RAW MUST NOT ESCAPE"),
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
const brokenSpeaker = createPromptBuilder({
|
|
147
|
+
buildSpeakerContext() { throw new Error("fixture speaker failure"); },
|
|
148
|
+
});
|
|
149
|
+
await assert.rejects(
|
|
150
|
+
() => brokenSpeaker.buildTurnPrompt("RAW MUST NOT ESCAPE", {
|
|
151
|
+
runContext: claudeContext,
|
|
152
|
+
provider: getProvider("claude"),
|
|
153
|
+
}),
|
|
154
|
+
(error) => error instanceof PromptConstructionError && error.stage === "speakerContext",
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
const recallFailOpen = createPromptBuilder({
|
|
158
|
+
async buildRecallContext() { throw new Error("fixture optional recall failure"); },
|
|
159
|
+
});
|
|
160
|
+
const omitted = await recallFailOpen.buildTurnPrompt("RECALL FAILURE USER SENTINEL", {
|
|
161
|
+
runContext: claudeContext,
|
|
162
|
+
provider: getProvider("claude"),
|
|
163
|
+
});
|
|
164
|
+
assert.strictEqual(omitted.userPrompt, "RECALL FAILURE USER SENTINEL");
|
|
165
|
+
assert.strictEqual(omitted.recallMetadata.status, "omitted");
|
|
166
|
+
assert.strictEqual(omitted.recallMetadata.omitted, true);
|
|
167
|
+
assert.ok(omitted.coreInstructions.includes("## Delivery"));
|
|
168
|
+
assert.ok(omitted.dynamicContext.includes("## Runtime state"));
|
|
169
|
+
|
|
170
|
+
const concurrent = createPromptBuilder({
|
|
171
|
+
async buildRecallContext(prompt) {
|
|
172
|
+
await new Promise((resolve) => setTimeout(resolve, prompt.startsWith("SLOW") ? 20 : 1));
|
|
173
|
+
return {
|
|
174
|
+
dynamicContext: `RECALL ${prompt}`,
|
|
175
|
+
metadata: { status: "included", omitted: false, marker: prompt },
|
|
176
|
+
};
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
const [slow, fast] = await Promise.all([
|
|
180
|
+
concurrent.buildTurnPrompt("SLOW RECALL", { runContext: claudeContext, provider: getProvider("claude") }),
|
|
181
|
+
concurrent.buildTurnPrompt("FAST RECALL", { runContext: codexContext, provider: getProvider("codex") }),
|
|
182
|
+
]);
|
|
183
|
+
assert.strictEqual(slow.recallMetadata.marker, "SLOW RECALL");
|
|
184
|
+
assert.strictEqual(fast.recallMetadata.marker, "FAST RECALL");
|
|
185
|
+
assert.ok(slow.dynamicContext.includes("RECALL SLOW RECALL"));
|
|
186
|
+
assert.ok(fast.dynamicContext.includes("RECALL FAST RECALL"));
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (process.argv[2] === "--probe") {
|
|
191
|
+
probe().then(() => console.log("provider core prompt OK")).catch((error) => {
|
|
192
|
+
console.error(error.stack || error.message);
|
|
193
|
+
process.exit(1);
|
|
194
|
+
});
|
|
195
|
+
} else {
|
|
196
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "open-claudia-core-prompt-"));
|
|
197
|
+
const configDir = path.join(root, "config");
|
|
198
|
+
const workspace = path.join(root, "workspace");
|
|
199
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
200
|
+
fs.mkdirSync(workspace, { recursive: true });
|
|
201
|
+
fs.writeFileSync(path.join(configDir, "soul.md"), "SOUL POLICY SENTINEL\n");
|
|
202
|
+
fs.writeFileSync(path.join(configDir, "persona.md"), "PERSONA SENTINEL — warm, direct, careful, and consistent across every supported provider.\n");
|
|
203
|
+
fs.writeFileSync(path.join(configDir, "lessons.md"), "- LESSON SENTINEL (src: fixture-pack)\n");
|
|
204
|
+
fs.writeFileSync(path.join(configDir, "people.json"), JSON.stringify([{
|
|
205
|
+
id: "fixture-person",
|
|
206
|
+
name: "Fixture Speaker",
|
|
207
|
+
isOwner: true,
|
|
208
|
+
bio: "Prompt parity tester",
|
|
209
|
+
handles: [{ adapter: "telegram", channelId: "fixture-channel", canonicalUserId: "fixture-user" }],
|
|
210
|
+
notes: [],
|
|
211
|
+
primaryChannel: { adapter: "telegram", channelId: "fixture-channel" },
|
|
212
|
+
}]));
|
|
213
|
+
|
|
214
|
+
const childEnv = {
|
|
215
|
+
HOME: root,
|
|
216
|
+
PATH: path.dirname(process.execPath),
|
|
217
|
+
TMPDIR: process.env.TMPDIR || os.tmpdir(),
|
|
218
|
+
TZ: "UTC",
|
|
219
|
+
OPEN_CLAUDIA_TEST: "1",
|
|
220
|
+
OPEN_CLAUDIA_CONFIG_DIR: configDir,
|
|
221
|
+
PROJECT_TRANSCRIPTS: "1",
|
|
222
|
+
RECALL_DEBUG: "0",
|
|
223
|
+
WORKSPACE: workspace,
|
|
224
|
+
CLAUDE_PATH: process.execPath,
|
|
225
|
+
CODEX_PATH: process.execPath,
|
|
226
|
+
};
|
|
227
|
+
const child = spawnSync(process.execPath, [__filename, "--probe"], {
|
|
228
|
+
cwd: __dirname,
|
|
229
|
+
env: childEnv,
|
|
230
|
+
encoding: "utf8",
|
|
231
|
+
timeout: 20000,
|
|
232
|
+
maxBuffer: 2 * 1024 * 1024,
|
|
233
|
+
});
|
|
234
|
+
try {
|
|
235
|
+
assert.ifError(child.error);
|
|
236
|
+
assert.strictEqual(child.status, 0, child.stderr || "provider core prompt probe failed");
|
|
237
|
+
assert.match(child.stdout, /provider core prompt OK/);
|
|
238
|
+
process.stdout.write(child.stdout);
|
|
239
|
+
|
|
240
|
+
const failureProbe = [
|
|
241
|
+
"const { createPromptBuilder } = require('./core/system-prompt');",
|
|
242
|
+
"const state = { userId: 'fixture-user', currentSession: { name: 'fixture', dir: process.cwd() }, settings: { backend: 'claude' }, activeSessions: {}, providerSettings: {} };",
|
|
243
|
+
"const runContext = { canonicalUserId: 'fixture-user', project: { name: 'fixture', dir: process.cwd() }, provider: 'claude', sessionId: null, runId: 'failure-probe' };",
|
|
244
|
+
"createPromptBuilder().buildTurnPrompt('RAW MUST NOT ESCAPE', { state, runContext, provider: { id: 'claude', nativeToolsNote: 'fixture' } }).then(() => process.exit(2), (error) => {",
|
|
245
|
+
" if (error.code !== 'PROMPT_CONSTRUCTION_FAILED' || error.stage !== 'coreInstructions' || String(error.bundle || '').includes('RAW MUST NOT ESCAPE')) process.exit(3);",
|
|
246
|
+
" process.stdout.write('mandatory soul failure closed\\n');",
|
|
247
|
+
"});",
|
|
248
|
+
].join("\n");
|
|
249
|
+
for (const contents of [null, ""]) {
|
|
250
|
+
if (contents === null) fs.rmSync(path.join(configDir, "soul.md"), { force: true });
|
|
251
|
+
else fs.writeFileSync(path.join(configDir, "soul.md"), contents);
|
|
252
|
+
const failed = spawnSync(process.execPath, ["-e", failureProbe], {
|
|
253
|
+
cwd: __dirname,
|
|
254
|
+
env: childEnv,
|
|
255
|
+
encoding: "utf8",
|
|
256
|
+
timeout: 20000,
|
|
257
|
+
});
|
|
258
|
+
assert.strictEqual(failed.status, 0, failed.stderr || "missing/empty soul must fail closed");
|
|
259
|
+
assert.match(failed.stdout, /mandatory soul failure closed/);
|
|
260
|
+
}
|
|
261
|
+
} finally {
|
|
262
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
263
|
+
}
|
|
264
|
+
}
|