@inetafrica/open-claudia 2.14.8 → 3.0.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/.env.example +30 -4
- package/CHANGELOG.md +21 -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 +105 -7
- package/channels/telegram/adapter.js +6 -1
- package/channels/voice/adapter.js +1 -0
- package/channels/voice/manage.js +43 -5
- package/config-dir.js +3 -11
- package/core/actions.js +149 -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 +127 -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 +55 -0
- package/core/handlers.js +503 -217
- package/core/ideas.js +2 -1
- package/core/identity.js +8 -11
- package/core/io.js +24 -3
- package/core/jobs.js +589 -72
- package/core/lessons.js +3 -2
- package/core/loopback.js +45 -8
- 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 +117 -0
- package/core/providers/claude-hook.js +114 -0
- package/core/providers/claude.js +296 -0
- package/core/providers/codex-events.js +121 -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 +170 -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 +1415 -1282
- package/core/scheduler.js +515 -210
- package/core/side-chat.js +346 -0
- package/core/state.js +1096 -95
- 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/core/web-sessions.js +78 -0
- 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 +51 -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 +334 -0
- package/test-delivery-contract.js +88 -0
- package/test-enforcer.js +70 -27
- package/test-fixtures/current-provider-parity.json +132 -0
- package/test-fixtures/fake-agent-cli.js +477 -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 +141 -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-run-lock.js +63 -0
- package/test-runner-watchdog-static.js +16 -8
- package/test-single-runtime.js +35 -0
- package/test-telegram-poll-recovery.js +56 -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/test-web-sessions.js +74 -0
- package/web.js +159 -45
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"users":{"broken":{"currentSession":
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
[
|
|
2
|
+
{ "id": "job-one", "kind": "wakeup", "canonicalUserId": "telegram:100", "project": "one", "sessionKey": "lastSessionId", "sessionId": "session-one", "prompt": "one" },
|
|
3
|
+
{ "id": "job-two", "kind": "cron", "canonicalUserId": "person@example.test", "project": "two", "sessionKey": "codexSessionId", "sessionId": "thread-two", "prompt": "two" }
|
|
4
|
+
]
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{
|
|
2
|
+
"users": {
|
|
3
|
+
"telegram:100": { "currentSession": { "name": "one", "dir": "/workspace/one" }, "lastSessionId": "session-one", "settings": { "backend": "claude" } },
|
|
4
|
+
"person@example.test": { "currentSession": { "name": "two", "dir": "/workspace/two" }, "codexSessionId": "thread-two", "settings": { "backend": "codex" } }
|
|
5
|
+
}
|
|
6
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"claude": {
|
|
3
|
+
"session": { "type": "system", "subtype": "init", "session_id": "claude-session-1" },
|
|
4
|
+
"delta": { "type": "stream_event", "event": { "type": "content_block_delta", "delta": { "type": "text_delta", "text": "Hello " } }, "session_id": "claude-session-1" },
|
|
5
|
+
"assistant": { "type": "assistant", "message": { "role": "assistant", "content": [{ "type": "text", "text": "world" }], "usage": { "input_tokens": 12, "output_tokens": 3 } }, "session_id": "claude-session-1" },
|
|
6
|
+
"toolUse": { "type": "assistant", "message": { "role": "assistant", "content": [{ "type": "tool_use", "id": "tool-1", "name": "Read", "input": { "file_path": "fixture.txt" } }] }, "session_id": "claude-session-1" },
|
|
7
|
+
"toolResult": { "type": "user", "message": { "role": "user", "content": [{ "type": "tool_result", "tool_use_id": "tool-1", "content": "fixture output", "is_error": false }] }, "session_id": "claude-session-1" },
|
|
8
|
+
"result": { "type": "result", "subtype": "success", "is_error": false, "session_id": "claude-session-1", "result": "Hello world", "usage": { "input_tokens": 12, "output_tokens": 3 }, "total_cost_usd": 0 }
|
|
9
|
+
},
|
|
10
|
+
"codex": {
|
|
11
|
+
"session": { "type": "thread.started", "thread_id": "codex-thread-1" },
|
|
12
|
+
"toolStart": { "type": "item.started", "item": { "id": "item-1", "type": "command_execution", "command": "printf fixture", "status": "in_progress" } },
|
|
13
|
+
"toolEnd": { "type": "item.completed", "item": { "id": "item-1", "type": "command_execution", "command": "printf fixture", "aggregated_output": "fixture", "exit_code": 0, "status": "completed" } },
|
|
14
|
+
"assistant": { "type": "item.completed", "item": { "id": "item-final", "type": "agent_message", "text": "Hello from Codex" } },
|
|
15
|
+
"result": { "type": "turn.completed", "usage": { "input_tokens": 20, "cached_input_tokens": 5, "output_tokens": 4 } }
|
|
16
|
+
}
|
|
17
|
+
}
|
package/test-learning-e2e.js
CHANGED
|
@@ -73,6 +73,11 @@ if (haveFts) {
|
|
|
73
73
|
const transferred = packs.recordCoUse(["pack:mobile-app-deploy", "pack:chat-mobile"]);
|
|
74
74
|
assert.deepStrictEqual(transferred.map((t) => t.project), ["chat-mobile"], "Stage 4: reuse recorded from real use");
|
|
75
75
|
assert.deepStrictEqual(packs.readPack("mobile-app-deploy").applied_on, ["spaces", "chat-mobile"], "Stage 4: applied_on grew");
|
|
76
|
+
const learnedAbilityFile = path.join(process.env.PACKS_DIR, "mobile-app-deploy", "PACK.md");
|
|
77
|
+
assert.ok(fs.existsSync(`${learnedAbilityFile}.bak`), "learning updates retain a last-good pack backup");
|
|
78
|
+
assert.strictEqual(fs.statSync(learnedAbilityFile).mode & 0o777, 0o600, "learning files stay owner-only");
|
|
79
|
+
assert.deepStrictEqual(fs.readdirSync(path.dirname(learnedAbilityFile)).filter((name) => name.endsWith(".tmp")), [],
|
|
80
|
+
"atomic learning writes leave no partial temporary file");
|
|
76
81
|
if (haveGraph) {
|
|
77
82
|
graph.syncFromCorpus(packs, entStub);
|
|
78
83
|
const fromChat = graph.expand([{ id: "pack:chat-mobile", score: 4 }], {});
|
|
@@ -0,0 +1,191 @@
|
|
|
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
|
+
|
|
10
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "open-claudia-memory-queue-"));
|
|
11
|
+
process.env.OPEN_CLAUDIA_TEST = "1";
|
|
12
|
+
process.env.OPEN_CLAUDIA_CONFIG_DIR = path.join(root, "config");
|
|
13
|
+
fs.mkdirSync(process.env.OPEN_CLAUDIA_CONFIG_DIR, { recursive: true });
|
|
14
|
+
|
|
15
|
+
const {
|
|
16
|
+
MemoryMutationQueue,
|
|
17
|
+
MutationQueueError,
|
|
18
|
+
} = require("./core/memory-mutation-queue");
|
|
19
|
+
const { atomicWriteFileSync } = require("./core/fsutil");
|
|
20
|
+
|
|
21
|
+
function deferred() {
|
|
22
|
+
let resolve;
|
|
23
|
+
let reject;
|
|
24
|
+
const promise = new Promise((res, rej) => { resolve = res; reject = rej; });
|
|
25
|
+
return { promise, resolve, reject };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function wait(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); }
|
|
29
|
+
|
|
30
|
+
async function main() {
|
|
31
|
+
const lockDir = path.join(root, "shared.lock");
|
|
32
|
+
const journalFile = path.join(root, "mutation-journal.jsonl");
|
|
33
|
+
const firstQueue = new MemoryMutationQueue({ lockDir, journalFile, maxPending: 5, lockPollMs: 5 });
|
|
34
|
+
const secondQueue = new MemoryMutationQueue({ lockDir, journalFile, maxPending: 5, lockPollMs: 5 });
|
|
35
|
+
const gate = deferred();
|
|
36
|
+
const events = [];
|
|
37
|
+
|
|
38
|
+
const first = firstQueue.enqueue("review", async () => {
|
|
39
|
+
events.push("review:start");
|
|
40
|
+
await gate.promise;
|
|
41
|
+
events.push("review:end");
|
|
42
|
+
return "reviewed";
|
|
43
|
+
}, { provider: "codex", model: "fixture" });
|
|
44
|
+
while (!events.includes("review:start")) await wait(5);
|
|
45
|
+
const second = secondQueue.enqueue("dream", async () => {
|
|
46
|
+
events.push("dream:start");
|
|
47
|
+
await wait(5);
|
|
48
|
+
events.push("dream:end");
|
|
49
|
+
return "dreamed";
|
|
50
|
+
});
|
|
51
|
+
await wait(30);
|
|
52
|
+
assert.deepStrictEqual(events, ["review:start"], "independent queue instances serialize through the shared lock");
|
|
53
|
+
gate.resolve();
|
|
54
|
+
assert.deepStrictEqual(await Promise.all([first, second]), ["reviewed", "dreamed"]);
|
|
55
|
+
assert.deepStrictEqual(events, ["review:start", "review:end", "dream:start", "dream:end"]);
|
|
56
|
+
assert.ok(!fs.existsSync(lockDir), "shared lock is removed after every mutation");
|
|
57
|
+
|
|
58
|
+
const liveLockDir = path.join(root, "live-stale.lock");
|
|
59
|
+
const liveGate = deferred();
|
|
60
|
+
const liveOwner = new MemoryMutationQueue({
|
|
61
|
+
lockDir: liveLockDir,
|
|
62
|
+
journalFile,
|
|
63
|
+
maxPending: 2,
|
|
64
|
+
lockPollMs: 5,
|
|
65
|
+
staleLockMs: 10,
|
|
66
|
+
});
|
|
67
|
+
const liveContender = new MemoryMutationQueue({
|
|
68
|
+
lockDir: liveLockDir,
|
|
69
|
+
journalFile,
|
|
70
|
+
maxPending: 2,
|
|
71
|
+
lockPollMs: 5,
|
|
72
|
+
staleLockMs: 10,
|
|
73
|
+
lockTimeoutMs: 50,
|
|
74
|
+
});
|
|
75
|
+
const liveMutation = liveOwner.enqueue("long-dream", async () => liveGate.promise);
|
|
76
|
+
while (!fs.existsSync(path.join(liveLockDir, "owner.json"))) await wait(5);
|
|
77
|
+
await wait(20);
|
|
78
|
+
await assert.rejects(
|
|
79
|
+
liveContender.enqueue("must-not-steal", async () => "stolen"),
|
|
80
|
+
(error) => error.code === "MEMORY_MUTATION_LOCK_TIMEOUT",
|
|
81
|
+
"an old lock owned by a live process cannot be stolen from a long dream mutation",
|
|
82
|
+
);
|
|
83
|
+
liveGate.resolve("complete");
|
|
84
|
+
assert.strictEqual(await liveMutation, "complete");
|
|
85
|
+
|
|
86
|
+
const deadLockDir = path.join(root, "dead-stale.lock");
|
|
87
|
+
fs.mkdirSync(deadLockDir);
|
|
88
|
+
fs.writeFileSync(path.join(deadLockDir, "owner.json"), JSON.stringify({ token: "dead", pid: 2147483647 }));
|
|
89
|
+
const old = new Date(Date.now() - 10_000);
|
|
90
|
+
fs.utimesSync(deadLockDir, old, old);
|
|
91
|
+
const deadRecovery = new MemoryMutationQueue({
|
|
92
|
+
lockDir: deadLockDir,
|
|
93
|
+
journalFile,
|
|
94
|
+
lockPollMs: 5,
|
|
95
|
+
staleLockMs: 10,
|
|
96
|
+
lockTimeoutMs: 200,
|
|
97
|
+
});
|
|
98
|
+
assert.strictEqual(await deadRecovery.enqueue("recover-dead-lock", async () => "recovered"), "recovered");
|
|
99
|
+
|
|
100
|
+
const failureQueue = new MemoryMutationQueue({
|
|
101
|
+
lockDir: path.join(root, "failure.lock"),
|
|
102
|
+
journalFile,
|
|
103
|
+
maxPending: 3,
|
|
104
|
+
lockPollMs: 5,
|
|
105
|
+
});
|
|
106
|
+
await assert.rejects(
|
|
107
|
+
failureQueue.enqueue("broken-review", async () => { throw new Error("write fixture failed"); }),
|
|
108
|
+
/write fixture failed/,
|
|
109
|
+
);
|
|
110
|
+
assert.strictEqual(await failureQueue.enqueue("after-failure", async () => "still runs"), "still runs",
|
|
111
|
+
"one failed mutation never poisons the serial chain");
|
|
112
|
+
|
|
113
|
+
const boundedGate = deferred();
|
|
114
|
+
const bounded = new MemoryMutationQueue({
|
|
115
|
+
lockDir: path.join(root, "bounded.lock"),
|
|
116
|
+
journalFile,
|
|
117
|
+
maxPending: 1,
|
|
118
|
+
lockPollMs: 5,
|
|
119
|
+
});
|
|
120
|
+
const held = bounded.enqueue("held", async () => boundedGate.promise);
|
|
121
|
+
await wait(10);
|
|
122
|
+
await assert.rejects(
|
|
123
|
+
bounded.enqueue("overflow", async () => {}),
|
|
124
|
+
(error) => error instanceof MutationQueueError && error.code === "MEMORY_MUTATION_QUEUE_FULL",
|
|
125
|
+
);
|
|
126
|
+
boundedGate.resolve("released");
|
|
127
|
+
assert.strictEqual(await held, "released");
|
|
128
|
+
|
|
129
|
+
const shutdownGate = deferred();
|
|
130
|
+
const shutdownQueue = new MemoryMutationQueue({
|
|
131
|
+
lockDir: path.join(root, "shutdown.lock"),
|
|
132
|
+
journalFile,
|
|
133
|
+
maxPending: 3,
|
|
134
|
+
lockPollMs: 5,
|
|
135
|
+
});
|
|
136
|
+
const pending = shutdownQueue.enqueue("pending-review", async () => shutdownGate.promise, { provider: "claude" });
|
|
137
|
+
await wait(10);
|
|
138
|
+
shutdownQueue.stopAdmission("test shutdown");
|
|
139
|
+
await assert.rejects(
|
|
140
|
+
shutdownQueue.enqueue("late-review", async () => {}),
|
|
141
|
+
(error) => error.code === "MEMORY_MUTATION_QUEUE_CLOSED",
|
|
142
|
+
);
|
|
143
|
+
const timedDrain = await shutdownQueue.drain({ timeoutMs: 25 });
|
|
144
|
+
assert.strictEqual(timedDrain.drained, false);
|
|
145
|
+
assert.strictEqual(timedDrain.pending.length, 1);
|
|
146
|
+
assert.strictEqual(timedDrain.pending[0].label, "pending-review");
|
|
147
|
+
const journalBeforeRelease = fs.readFileSync(journalFile, "utf8");
|
|
148
|
+
assert.match(journalBeforeRelease, /"status":"pending_on_shutdown"/);
|
|
149
|
+
assert.ok(!journalBeforeRelease.includes("provider prompt"), "queue journal stores metadata only, never prompts");
|
|
150
|
+
shutdownGate.resolve("done");
|
|
151
|
+
assert.strictEqual(await pending, "done");
|
|
152
|
+
const finalDrain = await shutdownQueue.drain({ timeoutMs: 100 });
|
|
153
|
+
assert.strictEqual(finalDrain.drained, true);
|
|
154
|
+
assert.deepStrictEqual(finalDrain.pending, []);
|
|
155
|
+
|
|
156
|
+
const atomicFile = path.join(root, "atomic", "memory.md");
|
|
157
|
+
atomicWriteFileSync(atomicFile, "old complete memory\n", { mode: 0o600, backup: true });
|
|
158
|
+
const originalRename = fs.renameSync;
|
|
159
|
+
fs.renameSync = () => { throw new Error("rename fault fixture"); };
|
|
160
|
+
try {
|
|
161
|
+
assert.throws(
|
|
162
|
+
() => atomicWriteFileSync(atomicFile, "new incomplete memory\n", { mode: 0o600, backup: true }),
|
|
163
|
+
/rename fault fixture/,
|
|
164
|
+
);
|
|
165
|
+
} finally {
|
|
166
|
+
fs.renameSync = originalRename;
|
|
167
|
+
}
|
|
168
|
+
assert.strictEqual(fs.readFileSync(atomicFile, "utf8"), "old complete memory\n",
|
|
169
|
+
"failed atomic replacement preserves the complete previous file");
|
|
170
|
+
assert.deepStrictEqual(
|
|
171
|
+
fs.readdirSync(path.dirname(atomicFile)).filter((name) => name.endsWith(".tmp")),
|
|
172
|
+
[],
|
|
173
|
+
"failed atomic replacement cleans its temporary file",
|
|
174
|
+
);
|
|
175
|
+
atomicWriteFileSync(atomicFile, "new complete memory\n", { mode: 0o600, backup: true });
|
|
176
|
+
assert.strictEqual(fs.readFileSync(atomicFile, "utf8"), "new complete memory\n");
|
|
177
|
+
assert.strictEqual(fs.readFileSync(`${atomicFile}.bak`, "utf8"), "old complete memory\n");
|
|
178
|
+
assert.strictEqual(fs.statSync(atomicFile).mode & 0o777, 0o600);
|
|
179
|
+
|
|
180
|
+
const journal = fs.readFileSync(journalFile, "utf8").trim().split("\n").map(JSON.parse);
|
|
181
|
+
assert.ok(journal.some((entry) => entry.label === "broken-review" && entry.status === "failed"));
|
|
182
|
+
assert.ok(journal.some((entry) => entry.label === "after-failure" && entry.status === "completed"));
|
|
183
|
+
assert.ok(journal.every((entry) => !Object.prototype.hasOwnProperty.call(entry, "payload")));
|
|
184
|
+
|
|
185
|
+
console.log("memory mutation queue OK");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
main().catch((error) => {
|
|
189
|
+
console.error(error.stack || error.message);
|
|
190
|
+
process.exitCode = 1;
|
|
191
|
+
}).finally(() => fs.rmSync(root, { recursive: true, force: true }));
|
|
@@ -0,0 +1,399 @@
|
|
|
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 http = require("http");
|
|
10
|
+
const { spawnSync } = require("child_process");
|
|
11
|
+
|
|
12
|
+
const root = __dirname;
|
|
13
|
+
|
|
14
|
+
function writeProviderCli(filePath, kind, mode, logPath) {
|
|
15
|
+
const source = `#!/usr/bin/env node
|
|
16
|
+
"use strict";
|
|
17
|
+
const fs = require("fs");
|
|
18
|
+
const args = process.argv.slice(2);
|
|
19
|
+
fs.appendFileSync(${JSON.stringify(logPath)}, JSON.stringify({ kind: ${JSON.stringify(kind)}, args }) + "\\n");
|
|
20
|
+
const same = (...wanted) => args.length === wanted.length && args.every((value, index) => value === wanted[index]);
|
|
21
|
+
if (same("--version")) {
|
|
22
|
+
process.stdout.write(${JSON.stringify(kind === "claude" ? "1.2.3\n" : "codex-cli 0.144.0\n")});
|
|
23
|
+
} else if (${JSON.stringify(kind)} === "claude" && same("--help")) {
|
|
24
|
+
process.stdout.write(${JSON.stringify(mode === "incompatible"
|
|
25
|
+
? "--output-format --resume --permission-mode --settings\n"
|
|
26
|
+
: "--append-system-prompt --output-format --resume --permission-mode --settings\n")});
|
|
27
|
+
} else if (${JSON.stringify(kind)} === "claude" && same("auth", "status")) {
|
|
28
|
+
if (${JSON.stringify(mode)} === "unauthenticated") {
|
|
29
|
+
process.stderr.write("not logged in\\n");
|
|
30
|
+
process.exitCode = 1;
|
|
31
|
+
} else process.stdout.write("authenticated via fixture store\\n");
|
|
32
|
+
} else if (${JSON.stringify(kind)} === "codex" && (same("exec", "--help") || same("exec", "resume", "--help"))) {
|
|
33
|
+
process.stdout.write(${JSON.stringify(mode === "incompatible"
|
|
34
|
+
? "--config --json --sandbox --model --skip-git-repo-check --strict-config --ignore-user-config --dangerously-bypass-hook-trust\n"
|
|
35
|
+
: "--config --json --sandbox --image --model --skip-git-repo-check --strict-config --ignore-user-config --dangerously-bypass-hook-trust\n")});
|
|
36
|
+
} else if (${JSON.stringify(kind)} === "codex" && args[0] === "features" && args.at(-1) === "list") {
|
|
37
|
+
process.stdout.write("hooks stable true\\nplugins stable false\\nunified_exec stable false\\n");
|
|
38
|
+
} else if (${JSON.stringify(kind)} === "codex" && same("login", "status")) {
|
|
39
|
+
if (${JSON.stringify(mode)} === "unauthenticated") {
|
|
40
|
+
process.stderr.write("not logged in\\n");
|
|
41
|
+
process.exitCode = 1;
|
|
42
|
+
} else process.stdout.write("logged in\\n");
|
|
43
|
+
} else {
|
|
44
|
+
process.stderr.write("MODEL_PROMPT_INVOCATION " + JSON.stringify(args) + "\\n");
|
|
45
|
+
process.exitCode = 70;
|
|
46
|
+
}
|
|
47
|
+
`;
|
|
48
|
+
fs.writeFileSync(filePath, source, { mode: 0o755 });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function readCommandLog(logPath) {
|
|
52
|
+
if (!fs.existsSync(logPath)) return [];
|
|
53
|
+
return fs.readFileSync(logPath, "utf8")
|
|
54
|
+
.split(/\r?\n/)
|
|
55
|
+
.filter(Boolean)
|
|
56
|
+
.map((line) => JSON.parse(line));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function isLocalStatusProbe(entry) {
|
|
60
|
+
const joined = entry.args.join(" ");
|
|
61
|
+
if (entry.kind === "claude") {
|
|
62
|
+
return joined === "--version" || joined === "--help" || joined === "auth status";
|
|
63
|
+
}
|
|
64
|
+
return joined === "--version"
|
|
65
|
+
|| joined === "exec --help"
|
|
66
|
+
|| joined === "exec resume --help"
|
|
67
|
+
|| joined === "login status"
|
|
68
|
+
|| (entry.args[0] === "features" && entry.args.at(-1) === "list");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function childProbe() {
|
|
72
|
+
const config = require("./core/config");
|
|
73
|
+
const providers = require("./core/providers");
|
|
74
|
+
const setup = require("./setup");
|
|
75
|
+
const health = require("./health");
|
|
76
|
+
const doctor = require("./core/doctor");
|
|
77
|
+
const web = require("./web");
|
|
78
|
+
const stateApi = require("./core/state");
|
|
79
|
+
const { captureRunContext } = require("./core/run-context");
|
|
80
|
+
|
|
81
|
+
const selection = providers.selectDefaultProvider(config.DEFAULT_PROVIDER);
|
|
82
|
+
const freshState = stateApi.currentState();
|
|
83
|
+
const stateProvider = stateApi.getActiveProvider(freshState);
|
|
84
|
+
const setupStatus = setup.inspectProviderSetup();
|
|
85
|
+
const healthStatus = await health.runHealthChecks({ quick: false, skipNetwork: true });
|
|
86
|
+
const doctorChecks = doctor.runDoctorChecks();
|
|
87
|
+
const webStatus = web.providerConfigurationStatus();
|
|
88
|
+
|
|
89
|
+
// Startup, not merely import, must remain available without a provider.
|
|
90
|
+
const server = web.startWebServer();
|
|
91
|
+
if (!server.listening) await new Promise((resolve) => server.once("listening", resolve));
|
|
92
|
+
await new Promise((resolve) => server.close(resolve));
|
|
93
|
+
|
|
94
|
+
let afterDisappear = null;
|
|
95
|
+
if (process.env.DISAPPEAR_PROVIDER) {
|
|
96
|
+
const providerId = process.env.DISAPPEAR_PROVIDER;
|
|
97
|
+
const executable = config.discoverProviderExecutable(providerId);
|
|
98
|
+
if (executable) fs.rmSync(executable, { force: true });
|
|
99
|
+
afterDisappear = {
|
|
100
|
+
status: providers.providerStatus(providerId),
|
|
101
|
+
selection: providers.selectDefaultProvider(config.DEFAULT_PROVIDER),
|
|
102
|
+
};
|
|
103
|
+
try {
|
|
104
|
+
providers.resolveProviderForRun(providerId);
|
|
105
|
+
} catch (error) {
|
|
106
|
+
afterDisappear.runError = error.code;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let noProviderTurn = null;
|
|
111
|
+
if (!selection.providerId) {
|
|
112
|
+
let admissionError;
|
|
113
|
+
try {
|
|
114
|
+
captureRunContext({ state: freshState, prompt: "must not spawn", cwd: process.cwd() });
|
|
115
|
+
} catch (error) {
|
|
116
|
+
admissionError = error;
|
|
117
|
+
}
|
|
118
|
+
const sent = [];
|
|
119
|
+
const { runInChat } = require("./core/context");
|
|
120
|
+
const { deliverRouterError } = require("./core/router");
|
|
121
|
+
const delivered = await runInChat({
|
|
122
|
+
adapter: { type: "fixture", send: async (_channelId, message) => { sent.push(message); return true; } },
|
|
123
|
+
channelId: "fixture-chat",
|
|
124
|
+
canonicalUserId: "telegram:fixture-chat",
|
|
125
|
+
userId: "fixture-chat",
|
|
126
|
+
}, () => deliverRouterError(admissionError));
|
|
127
|
+
noProviderTurn = { admissionCode: admissionError?.code || null, delivered, sent };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
process.stdout.write(`BOOT_MATRIX:${JSON.stringify({
|
|
131
|
+
selection,
|
|
132
|
+
stateProvider,
|
|
133
|
+
setupStatus,
|
|
134
|
+
healthStatus,
|
|
135
|
+
doctorChecks,
|
|
136
|
+
webStatus,
|
|
137
|
+
afterDisappear,
|
|
138
|
+
noProviderTurn,
|
|
139
|
+
})}\n`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function httpRequest(port, pathname, options = {}) {
|
|
143
|
+
return new Promise((resolve, reject) => {
|
|
144
|
+
const body = options.body === undefined ? null : JSON.stringify(options.body);
|
|
145
|
+
const req = http.request({
|
|
146
|
+
hostname: "127.0.0.1",
|
|
147
|
+
port,
|
|
148
|
+
path: pathname,
|
|
149
|
+
method: options.method || "GET",
|
|
150
|
+
headers: {
|
|
151
|
+
...(body ? { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) } : {}),
|
|
152
|
+
...(options.cookie ? { Cookie: options.cookie } : {}),
|
|
153
|
+
},
|
|
154
|
+
}, (res) => {
|
|
155
|
+
let data = "";
|
|
156
|
+
res.setEncoding("utf8");
|
|
157
|
+
res.on("data", (chunk) => { data += chunk; });
|
|
158
|
+
res.on("end", () => resolve({
|
|
159
|
+
status: res.statusCode,
|
|
160
|
+
headers: res.headers,
|
|
161
|
+
body: data ? JSON.parse(data) : null,
|
|
162
|
+
}));
|
|
163
|
+
});
|
|
164
|
+
req.on("error", reject);
|
|
165
|
+
if (body) req.write(body);
|
|
166
|
+
req.end();
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function webHttpProbe() {
|
|
171
|
+
const web = require("./web");
|
|
172
|
+
const server = web.startWebServer({
|
|
173
|
+
telegramGet: async () => ({ ok: true, result: { id: 1, username: "fixture_bot", first_name: "Fixture" } }),
|
|
174
|
+
});
|
|
175
|
+
if (!server.listening) await new Promise((resolve) => server.once("listening", resolve));
|
|
176
|
+
const port = server.address().port;
|
|
177
|
+
const login = await httpRequest(port, "/api/login", {
|
|
178
|
+
method: "POST",
|
|
179
|
+
body: { password: process.env.WEB_PASSWORD },
|
|
180
|
+
});
|
|
181
|
+
assert.strictEqual(login.status, 200);
|
|
182
|
+
const cookie = String(login.headers["set-cookie"][0]).split(";")[0];
|
|
183
|
+
const setup = await httpRequest(port, "/api/setup", {
|
|
184
|
+
method: "POST",
|
|
185
|
+
cookie,
|
|
186
|
+
body: {
|
|
187
|
+
botToken: "fixture-token",
|
|
188
|
+
chatId: "fixture-chat",
|
|
189
|
+
workspace: process.env.WEB_FIXTURE_WORKSPACE,
|
|
190
|
+
claudePath: "",
|
|
191
|
+
codexPath: process.env.WEB_FIXTURE_CODEX,
|
|
192
|
+
defaultProvider: "codex",
|
|
193
|
+
utilityProvider: "active",
|
|
194
|
+
providerFallbacks: "claude",
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
assert.strictEqual(setup.status, 200, JSON.stringify(setup.body));
|
|
198
|
+
const status = await httpRequest(port, "/api/status", { cookie });
|
|
199
|
+
const configUpdate = await httpRequest(port, "/api/config", {
|
|
200
|
+
method: "POST",
|
|
201
|
+
cookie,
|
|
202
|
+
body: { UTILITY_PROVIDER: "codex", DREAM_PROVIDER: "codex" },
|
|
203
|
+
});
|
|
204
|
+
const config = await httpRequest(port, "/api/config", { cookie });
|
|
205
|
+
const soulUpdate = await httpRequest(port, "/api/soul", {
|
|
206
|
+
method: "POST",
|
|
207
|
+
cookie,
|
|
208
|
+
body: { content: "# Soul\n\nProvider-neutral fixture soul.\n" },
|
|
209
|
+
});
|
|
210
|
+
const soul = await httpRequest(port, "/api/soul", { cookie });
|
|
211
|
+
await new Promise((resolve) => server.close(resolve));
|
|
212
|
+
const envPath = path.join(process.env.OPEN_CLAUDIA_CONFIG_DIR, ".env");
|
|
213
|
+
process.stdout.write(`WEB_HTTP:${JSON.stringify({
|
|
214
|
+
setup: setup.body,
|
|
215
|
+
status: status.body,
|
|
216
|
+
configUpdate: configUpdate.body,
|
|
217
|
+
config: config.body,
|
|
218
|
+
soulUpdate: soulUpdate.body,
|
|
219
|
+
soul: soul.body,
|
|
220
|
+
envMode: fs.statSync(envPath).mode & 0o777,
|
|
221
|
+
})}\n`);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (process.argv[2] === "--probe") {
|
|
225
|
+
childProbe().catch((error) => {
|
|
226
|
+
process.stderr.write(`${error.stack || error.message}\n`);
|
|
227
|
+
process.exitCode = 1;
|
|
228
|
+
});
|
|
229
|
+
} else if (process.argv[2] === "--web-http-probe") {
|
|
230
|
+
webHttpProbe().catch((error) => {
|
|
231
|
+
process.stderr.write(`${error.stack || error.message}\n`);
|
|
232
|
+
process.exitCode = 1;
|
|
233
|
+
});
|
|
234
|
+
} else {
|
|
235
|
+
const scenarios = [
|
|
236
|
+
{ name: "claude-only", providers: { claude: "ok" }, expectedDefault: "claude" },
|
|
237
|
+
{ name: "codex-only", providers: { codex: "ok" }, expectedDefault: "codex" },
|
|
238
|
+
{ name: "codex-path-only", providers: { codex: "ok" }, pathOnly: true, expectedDefault: "codex" },
|
|
239
|
+
{ name: "dual", providers: { claude: "ok", codex: "ok" }, expectedDefault: "claude" },
|
|
240
|
+
{ name: "explicit-codex", providers: { claude: "ok", codex: "ok" }, defaultProvider: "codex", expectedDefault: "codex" },
|
|
241
|
+
{ name: "neither", providers: {}, expectedDefault: null },
|
|
242
|
+
{ name: "missing-executable", providers: { claude: "missing" }, expectedDefault: null },
|
|
243
|
+
{ name: "incompatible", providers: { claude: "incompatible" }, expectedDefault: null },
|
|
244
|
+
{ name: "unauthenticated", providers: { codex: "unauthenticated" }, expectedDefault: "codex", expectedAuth: "unauthenticated" },
|
|
245
|
+
{ name: "disappearing", providers: { claude: "ok" }, expectedDefault: "claude", disappear: "claude" },
|
|
246
|
+
];
|
|
247
|
+
|
|
248
|
+
const setupApi = require("./setup");
|
|
249
|
+
const resumed = setupApi.upgradeSetupState({
|
|
250
|
+
completedSteps: ["prerequisites", "telegram"],
|
|
251
|
+
data: { claudePath: "/legacy/claude" },
|
|
252
|
+
});
|
|
253
|
+
assert.ok(!resumed.completedSteps.includes("prerequisites"), "legacy setup state must revisit provider discovery");
|
|
254
|
+
assert.ok(resumed.completedSteps.includes("telegram"), "unrelated completed setup steps are preserved");
|
|
255
|
+
|
|
256
|
+
for (const scenario of scenarios) {
|
|
257
|
+
const temp = fs.mkdtempSync(path.join(os.tmpdir(), `open-claudia-boot-${scenario.name}-`));
|
|
258
|
+
const home = path.join(temp, "home");
|
|
259
|
+
const configDir = path.join(temp, "config");
|
|
260
|
+
const workspace = path.join(temp, "workspace");
|
|
261
|
+
const binDir = path.join(temp, "bin");
|
|
262
|
+
const commandLog = path.join(temp, "provider-commands.jsonl");
|
|
263
|
+
fs.mkdirSync(home);
|
|
264
|
+
fs.mkdirSync(configDir);
|
|
265
|
+
fs.mkdirSync(workspace);
|
|
266
|
+
fs.mkdirSync(binDir);
|
|
267
|
+
|
|
268
|
+
const envLines = [
|
|
269
|
+
"CHANNELS=",
|
|
270
|
+
"TELEGRAM_BOT_TOKEN=fixture-token",
|
|
271
|
+
"TELEGRAM_CHAT_ID=fixture-chat",
|
|
272
|
+
`WORKSPACE=${workspace}`,
|
|
273
|
+
];
|
|
274
|
+
for (const providerId of ["claude", "codex"]) {
|
|
275
|
+
const mode = scenario.providers[providerId];
|
|
276
|
+
if (!mode) continue;
|
|
277
|
+
const envKey = providerId === "claude" ? "CLAUDE_PATH" : "CODEX_PATH";
|
|
278
|
+
if (mode === "missing") {
|
|
279
|
+
envLines.push(`${envKey}=${path.join(binDir, `${providerId}-missing`)}`);
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
const executable = path.join(binDir, providerId);
|
|
283
|
+
writeProviderCli(executable, providerId, mode, commandLog);
|
|
284
|
+
if (!scenario.pathOnly) envLines.push(`${envKey}=${executable}`);
|
|
285
|
+
}
|
|
286
|
+
if (scenario.defaultProvider) envLines.push(`DEFAULT_PROVIDER=${scenario.defaultProvider}`);
|
|
287
|
+
fs.writeFileSync(path.join(configDir, ".env"), `${envLines.join("\n")}\n`);
|
|
288
|
+
|
|
289
|
+
const child = spawnSync(process.execPath, [__filename, "--probe"], {
|
|
290
|
+
cwd: root,
|
|
291
|
+
env: {
|
|
292
|
+
HOME: home,
|
|
293
|
+
PATH: [binDir, path.dirname(process.execPath), "/usr/bin", "/bin"].join(path.delimiter),
|
|
294
|
+
OPEN_CLAUDIA_CONFIG_DIR: configDir,
|
|
295
|
+
OPEN_CLAUDIA_TEST: "1",
|
|
296
|
+
WEB_PORT: "0",
|
|
297
|
+
DISAPPEAR_PROVIDER: scenario.disappear || "",
|
|
298
|
+
},
|
|
299
|
+
encoding: "utf8",
|
|
300
|
+
timeout: 30000,
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
try {
|
|
304
|
+
assert.ifError(child.error);
|
|
305
|
+
assert.strictEqual(child.status, 0, child.stderr || child.stdout);
|
|
306
|
+
const marker = child.stdout.split(/\r?\n/).find((line) => line.startsWith("BOOT_MATRIX:"));
|
|
307
|
+
assert.ok(marker, `${scenario.name}: missing boot-matrix output`);
|
|
308
|
+
const output = JSON.parse(marker.slice("BOOT_MATRIX:".length));
|
|
309
|
+
assert.strictEqual(output.selection.providerId, scenario.expectedDefault, `${scenario.name}: deterministic default`);
|
|
310
|
+
assert.strictEqual(output.stateProvider, scenario.expectedDefault, `${scenario.name}: fresh state adopts the default`);
|
|
311
|
+
assert.strictEqual(output.setupStatus.defaultProvider.providerId, scenario.expectedDefault, `${scenario.name}: terminal setup default`);
|
|
312
|
+
assert.strictEqual(output.webStatus.defaultProvider.providerId, scenario.expectedDefault, `${scenario.name}: web default`);
|
|
313
|
+
assert.strictEqual(output.healthStatus.providerSummary.defaultProvider.providerId, scenario.expectedDefault, `${scenario.name}: health default`);
|
|
314
|
+
|
|
315
|
+
for (const providerId of ["claude", "codex"]) {
|
|
316
|
+
const expectedMode = scenario.providers[providerId];
|
|
317
|
+
const setupProvider = output.setupStatus.providers.find((entry) => entry.id === providerId);
|
|
318
|
+
const webProvider = output.webStatus.providers.find((entry) => entry.id === providerId);
|
|
319
|
+
assert.ok(setupProvider && webProvider, `${scenario.name}: ${providerId} must be enumerated`);
|
|
320
|
+
assert.strictEqual(webProvider.availability.state, setupProvider.availability.state);
|
|
321
|
+
if (expectedMode === "missing") assert.strictEqual(setupProvider.availability.state, "missing");
|
|
322
|
+
else if (!expectedMode) assert.strictEqual(setupProvider.availability.state, "unconfigured");
|
|
323
|
+
else assert.strictEqual(setupProvider.availability.state, "available");
|
|
324
|
+
if (expectedMode === "incompatible") assert.strictEqual(setupProvider.compatibility.state, "incompatible");
|
|
325
|
+
if (providerId === scenario.expectedDefault && scenario.expectedAuth) {
|
|
326
|
+
assert.strictEqual(setupProvider.auth.state, scenario.expectedAuth);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const doctorLabels = output.doctorChecks.map((check) => check.label);
|
|
331
|
+
assert.ok(doctorLabels.includes("Claude Code availability"), `${scenario.name}: doctor enumerates Claude`);
|
|
332
|
+
assert.ok(doctorLabels.includes("OpenAI Codex availability"), `${scenario.name}: doctor enumerates Codex`);
|
|
333
|
+
assert.ok(doctorLabels.includes("Default provider"), `${scenario.name}: doctor reports the default choice`);
|
|
334
|
+
|
|
335
|
+
if (scenario.disappear) {
|
|
336
|
+
assert.strictEqual(output.afterDisappear.status.availability.state, "missing");
|
|
337
|
+
assert.strictEqual(output.afterDisappear.selection.providerId, null);
|
|
338
|
+
assert.strictEqual(output.afterDisappear.runError, "PROVIDER_UNAVAILABLE");
|
|
339
|
+
}
|
|
340
|
+
if (!scenario.expectedDefault) {
|
|
341
|
+
assert.strictEqual(output.noProviderTurn.admissionCode, "NO_AVAILABLE_PROVIDER");
|
|
342
|
+
assert.strictEqual(output.noProviderTurn.delivered.error.code, "NO_AVAILABLE_PROVIDER");
|
|
343
|
+
assert.strictEqual(output.noProviderTurn.sent.length, 1, `${scenario.name}: no-provider error is user-visible`);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const commands = readCommandLog(commandLog);
|
|
347
|
+
assert.ok(commands.every(isLocalStatusProbe), `${scenario.name}: health/setup invoked a model prompt: ${JSON.stringify(commands)}`);
|
|
348
|
+
} finally {
|
|
349
|
+
fs.rmSync(temp, { recursive: true, force: true });
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const webTemp = fs.mkdtempSync(path.join(os.tmpdir(), "open-claudia-web-boot-"));
|
|
354
|
+
try {
|
|
355
|
+
const configDir = path.join(webTemp, "new-config");
|
|
356
|
+
const home = path.join(webTemp, "home");
|
|
357
|
+
const binDir = path.join(webTemp, "bin");
|
|
358
|
+
const workspace = path.join(webTemp, "workspace");
|
|
359
|
+
const commandLog = path.join(webTemp, "provider-commands.jsonl");
|
|
360
|
+
fs.mkdirSync(home);
|
|
361
|
+
fs.mkdirSync(binDir);
|
|
362
|
+
const codexPath = path.join(binDir, "codex");
|
|
363
|
+
writeProviderCli(codexPath, "codex", "ok", commandLog);
|
|
364
|
+
const webChild = spawnSync(process.execPath, [__filename, "--web-http-probe"], {
|
|
365
|
+
cwd: root,
|
|
366
|
+
env: {
|
|
367
|
+
HOME: home,
|
|
368
|
+
PATH: [binDir, path.dirname(process.execPath), "/usr/bin", "/bin"].join(path.delimiter),
|
|
369
|
+
OPEN_CLAUDIA_CONFIG_DIR: configDir,
|
|
370
|
+
OPEN_CLAUDIA_TEST: "1",
|
|
371
|
+
WEB_PORT: "0",
|
|
372
|
+
WEB_PASSWORD: "FixturePass1!",
|
|
373
|
+
WEB_FIXTURE_CODEX: codexPath,
|
|
374
|
+
WEB_FIXTURE_WORKSPACE: workspace,
|
|
375
|
+
},
|
|
376
|
+
encoding: "utf8",
|
|
377
|
+
timeout: 30000,
|
|
378
|
+
});
|
|
379
|
+
assert.ifError(webChild.error);
|
|
380
|
+
assert.strictEqual(webChild.status, 0, webChild.stderr || webChild.stdout);
|
|
381
|
+
const marker = webChild.stdout.split(/\r?\n/).find((line) => line.startsWith("WEB_HTTP:"));
|
|
382
|
+
assert.ok(marker, "fresh web startup must complete HTTP setup probe");
|
|
383
|
+
const output = JSON.parse(marker.slice("WEB_HTTP:".length));
|
|
384
|
+
assert.strictEqual(output.setup.ok, true);
|
|
385
|
+
assert.strictEqual(output.status.defaultProvider.providerId, "codex", "web setup refreshes cached default status");
|
|
386
|
+
assert.strictEqual(output.status.providers.find((provider) => provider.id === "codex").availability.state, "available");
|
|
387
|
+
assert.strictEqual(output.configUpdate.ok, true, "web config POST is reachable");
|
|
388
|
+
assert.strictEqual(output.config.UTILITY_PROVIDER, "codex");
|
|
389
|
+
assert.strictEqual(output.config.DREAM_PROVIDER, "codex");
|
|
390
|
+
assert.strictEqual(output.soulUpdate.ok, true, "web soul POST is reachable");
|
|
391
|
+
assert.match(output.soul.content, /Provider-neutral fixture soul/);
|
|
392
|
+
assert.strictEqual(output.envMode, 0o600, "web config is private");
|
|
393
|
+
assert.ok(readCommandLog(commandLog).every(isLocalStatusProbe), "web setup/status must not invoke a model prompt");
|
|
394
|
+
} finally {
|
|
395
|
+
fs.rmSync(webTemp, { recursive: true, force: true });
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
console.log("provider boot matrix OK");
|
|
399
|
+
}
|