@vellumai/assistant 0.8.11 → 0.8.12-staging.2
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/ARCHITECTURE.md +15 -17
- package/README.md +0 -6
- package/bun.lock +6 -122
- package/node_modules/@vellumai/gateway-client/bun.lock +1 -0
- package/node_modules/@vellumai/gateway-client/package.json +3 -1
- package/node_modules/@vellumai/gateway-client/src/__tests__/gateway-client.test.ts +1 -1
- package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +87 -0
- package/node_modules/@vellumai/gateway-client/src/index.ts +3 -5
- package/openapi.yaml +633 -4
- package/package.json +1 -3
- package/src/__tests__/adaptive-thinking-repair.test.ts +185 -0
- package/src/__tests__/agent-loop-compaction-events.test.ts +7 -6
- package/src/__tests__/anthropic-provider.test.ts +129 -0
- package/src/__tests__/background-workers-disk-pressure.test.ts +4 -1
- package/src/__tests__/btw-routes.test.ts +7 -34
- package/src/__tests__/checker.test.ts +6 -12
- package/src/__tests__/config-loader-backfill.test.ts +4 -2
- package/src/__tests__/config-loader-quarantine-notice.test.ts +167 -0
- package/src/__tests__/config-watcher.test.ts +2 -2
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -1
- package/src/__tests__/conversation-error.test.ts +2 -6
- package/src/__tests__/conversation-history-web-search.test.ts +8 -0
- package/src/__tests__/conversation-title-service.test.ts +2 -1
- package/src/__tests__/credential-security-invariants.test.ts +1 -1
- package/src/__tests__/disk-pressure-tools.test.ts +1 -1
- package/src/__tests__/exploration-drift-hook.test.ts +692 -0
- package/src/__tests__/filing-service.test.ts +8 -3
- package/src/__tests__/guardian-action-store.test.ts +0 -167
- package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +1 -1
- package/src/__tests__/heartbeat-disk-pressure.test.ts +4 -1
- package/src/__tests__/heartbeat-service.test.ts +5 -2
- package/src/__tests__/identity-intro-cache.test.ts +12 -5
- package/src/__tests__/identity-routes.test.ts +16 -57
- package/src/__tests__/injector-chain.test.ts +8 -3
- package/src/__tests__/injector-config-quarantine-notice.test.ts +115 -0
- package/src/__tests__/llm-catalog-parity.test.ts +16 -0
- package/src/__tests__/llm-usage-store.test.ts +11 -0
- package/src/__tests__/log-export-workspace.test.ts +468 -3
- package/src/__tests__/memory-v2-static-injector.test.ts +22 -0
- package/src/__tests__/model-intents.test.ts +1 -1
- package/src/__tests__/oauth-cli.test.ts +19 -8
- package/src/__tests__/openai-provider.test.ts +34 -0
- package/src/__tests__/prechat-onboarding-contract.test.ts +0 -1
- package/src/__tests__/recurrence-engine.test.ts +45 -0
- package/src/__tests__/schedule-routes.test.ts +34 -0
- package/src/__tests__/scheduler-disk-pressure.test.ts +1 -1
- package/src/__tests__/script-proxy-conversation-manager.test.ts +10 -5
- package/src/__tests__/secret-fixtures.ts +20 -0
- package/src/__tests__/skill-tool-factory.test.ts +49 -0
- package/src/__tests__/subagent-role-registry.test.ts +24 -1
- package/src/__tests__/subagent-tools.test.ts +1 -0
- package/src/__tests__/system-prompt.test.ts +109 -11
- package/src/__tests__/tool-approval-handler.test.ts +85 -0
- package/src/__tests__/tool-audit-listener.test.ts +86 -0
- package/src/__tests__/tool-error-hook.test.ts +1 -0
- package/src/__tests__/tool-result-spool.test.ts +337 -0
- package/src/__tests__/tool-result-truncate-hook.test.ts +1 -0
- package/src/__tests__/validate-input.test.ts +95 -1
- package/src/__tests__/workspace-migration-098-remove-stale-updates-bulletin-file.test.ts +65 -0
- package/src/__tests__/workspace-migration-099-disable-cache-one-shot-callsites.test.ts +139 -0
- package/src/__tests__/workspace-migration-100-upgrade-quality-profile-to-fable-5.test.ts +174 -0
- package/src/__tests__/workspace-migration-101-upgrade-balanced-economy-to-minimax-m3.test.ts +162 -0
- package/src/__tests__/workspace-release-notes-feature-flag-guard.test.ts +45 -95
- package/src/acp/__tests__/agent-process.test.ts +315 -2
- package/src/acp/__tests__/prepare-agent-env.test.ts +79 -5
- package/src/acp/agent-process.ts +163 -34
- package/src/acp/prepare-agent-env.ts +55 -15
- package/src/agent/loop.ts +81 -24
- package/src/api/events/usage-progress.ts +28 -0
- package/src/api/index.ts +6 -0
- package/src/background-wake/wake-intent-hooks.test.ts +2 -0
- package/src/bundler/app-bundler.ts +25 -42
- package/src/bundler/app-compiler.ts +8 -0
- package/src/calls/call-controller.ts +1 -1
- package/src/cli/commands/plugins.ts +248 -15
- package/src/cli/lib/__tests__/inspect-plugin.test.ts +318 -0
- package/src/cli/lib/__tests__/install-from-github.test.ts +16 -9
- package/src/cli/lib/__tests__/plugin-artifact.test.ts +183 -0
- package/src/cli/lib/__tests__/plugin-details.test.ts +158 -0
- package/src/cli/lib/__tests__/plugin-fingerprint.test.ts +245 -0
- package/src/cli/lib/__tests__/upgrade-plugin.test.ts +307 -0
- package/src/cli/lib/inspect-plugin.ts +252 -0
- package/src/cli/lib/install-from-github.ts +214 -21
- package/src/cli/lib/list-installed-plugins.ts +17 -6
- package/src/cli/lib/plugin-artifact.ts +103 -0
- package/src/cli/lib/plugin-details.ts +18 -1
- package/src/cli/lib/plugin-fingerprint.ts +197 -0
- package/src/cli/lib/upgrade-plugin.ts +225 -0
- package/src/config/bundled-skills/subagent/SKILL.md +2 -0
- package/src/config/bundled-skills/subagent/TOOLS.json +8 -2
- package/src/config/call-site-defaults.ts +13 -2
- package/src/config/feature-flag-registry.json +8 -16
- package/src/config/loader.ts +52 -59
- package/src/config/schema.ts +0 -2
- package/src/config/schemas/__tests__/memory-v2.test.ts +1 -0
- package/src/config/schemas/__tests__/memory-v3.test.ts +10 -0
- package/src/config/schemas/llm.ts +10 -0
- package/src/config/schemas/memory-v2.ts +13 -0
- package/src/config/schemas/memory-v3.ts +92 -0
- package/src/config/seed-inference-profiles.ts +4 -8
- package/src/context/post-turn-tool-result-truncation.ts +32 -18
- package/src/context/tool-result-spool.ts +104 -0
- package/src/credential-execution/feature-gates.ts +0 -1
- package/src/daemon/conversation-agent-loop-handlers.ts +41 -16
- package/src/daemon/conversation-error.ts +6 -15
- package/src/daemon/conversation.ts +9 -0
- package/src/daemon/disk-pressure-policy.ts +0 -1
- package/src/daemon/lifecycle.ts +1 -20
- package/src/daemon/message-types/conversations.ts +2 -15
- package/src/daemon/trust-context.ts +1 -1
- package/src/events/tool-audit-listener.ts +40 -9
- package/src/heartbeat/__tests__/heartbeat-service.test.ts +1 -1
- package/src/home/__tests__/home-content-refresh.test.ts +114 -0
- package/src/home/__tests__/suggested-prompts.test.ts +86 -5
- package/src/home/home-content-refresh.ts +43 -31
- package/src/home/home-greeting-cache.ts +8 -1
- package/src/home/home-greeting.ts +13 -9
- package/src/home/suggested-prompts.ts +77 -24
- package/src/ipc/routes/trust-rules.test.ts +66 -72
- package/src/media/image-credentials.ts +2 -2
- package/src/memory/__tests__/compaction-log-store-clickhouse.test.ts +432 -0
- package/src/memory/{compaction-log-writer-clickhouse.ts → compaction-log-store-clickhouse.ts} +264 -55
- package/src/memory/conversation-attention-store.ts +1 -0
- package/src/memory/conversation-bootstrap.ts +18 -9
- package/src/memory/conversation-crud.ts +12 -2
- package/src/memory/conversation-title-service.ts +53 -9
- package/src/memory/delivery-channels.ts +0 -69
- package/src/memory/graph/extraction-job.ts +0 -15
- package/src/memory/guardian-action-store.ts +1 -376
- package/src/memory/llm-usage-store.ts +5 -1
- package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +2 -2
- package/src/memory/v2/__tests__/consolidation-job.test.ts +183 -2
- package/src/memory/v2/__tests__/injection.test.ts +70 -0
- package/src/memory/v2/__tests__/static-context.test.ts +12 -0
- package/src/memory/v2/consolidation-job.ts +93 -9
- package/src/memory/v2/injection.ts +53 -0
- package/src/memory/v2/prompts/consolidation.ts +1 -0
- package/src/memory/v2/static-context.ts +13 -1
- package/src/memory/v2/sweep-job.ts +1 -1
- package/src/memory/v2/types.ts +5 -0
- package/src/plugin-api/types.ts +7 -0
- package/src/plugins/defaults/exploration-drift/hooks/post-tool-use.ts +300 -0
- package/src/plugins/defaults/exploration-drift/package.json +15 -0
- package/src/plugins/defaults/index.ts +25 -0
- package/src/plugins/defaults/memory-retrieval/injectors.ts +132 -4
- package/src/plugins/defaults/memory-v3-shadow/__tests__/card.test.ts +92 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +2 -1
- package/src/plugins/defaults/memory-v3-shadow/__tests__/fresh-set.test.ts +52 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +1 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/live-integration.test.ts +2 -1
- package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +136 -5
- package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +17 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +6 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +5 -1
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +68 -4
- package/src/plugins/defaults/memory-v3-shadow/card.ts +49 -5
- package/src/plugins/defaults/memory-v3-shadow/fresh-set.ts +59 -0
- package/src/plugins/defaults/memory-v3-shadow/injector.ts +4 -2
- package/src/plugins/defaults/memory-v3-shadow/learned-edges.test.ts +169 -0
- package/src/plugins/defaults/memory-v3-shadow/learned-edges.ts +178 -0
- package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +115 -26
- package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +13 -9
- package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +144 -22
- package/src/plugins/defaults/memory-v3-shadow/types.ts +24 -6
- package/src/plugins/defaults/title-generate/hooks/stop.ts +13 -0
- package/src/plugins/defaults/title-generate/hooks/user-prompt-submit.ts +16 -0
- package/src/prompts/cache-boundary.ts +17 -0
- package/src/prompts/sections.ts +50 -17
- package/src/prompts/system-prompt.ts +12 -4
- package/src/prompts/templates/system-sections.ts +22 -0
- package/src/providers/__tests__/unparseable-tool-args.test.ts +53 -0
- package/src/providers/anthropic/client.ts +74 -28
- package/src/providers/gemini/client.ts +5 -1
- package/src/providers/minimax/client.ts +9 -0
- package/src/providers/model-catalog.ts +28 -0
- package/src/providers/model-intents.ts +3 -3
- package/src/providers/openai/chat-completions-provider.ts +4 -2
- package/src/providers/openai/responses-provider.ts +7 -2
- package/src/providers/retry.ts +8 -0
- package/src/providers/types.ts +11 -0
- package/src/providers/unparseable-tool-args.ts +56 -0
- package/src/runtime/AGENTS.md +6 -0
- package/src/runtime/__tests__/agent-wake.test.ts +2 -2
- package/src/runtime/agent-wake.ts +5 -5
- package/src/runtime/background-job-runner.ts +2 -2
- package/src/runtime/migrations/__tests__/vbundle-legacy-user-md.test.ts +150 -3
- package/src/runtime/migrations/vbundle-import-analyzer.ts +29 -6
- package/src/runtime/migrations/vbundle-import-policy.ts +23 -0
- package/src/runtime/migrations/vbundle-importer.ts +9 -4
- package/src/runtime/migrations/vbundle-streaming-importer.ts +8 -3
- package/src/runtime/pre-first-message-gate.ts +1 -1
- package/src/runtime/routes/__tests__/conversation-compaction-routes.test.ts +241 -0
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +132 -0
- package/src/runtime/routes/__tests__/gateway-log-routes.test.ts +97 -185
- package/src/runtime/routes/__tests__/home-feed-routes.test.ts +17 -0
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +348 -0
- package/src/runtime/routes/__tests__/task-routes.test.ts +3 -3
- package/src/runtime/routes/btw-routes.ts +0 -14
- package/src/runtime/routes/conversation-compaction-routes.ts +86 -19
- package/src/runtime/routes/conversation-list-routes.ts +77 -5
- package/src/runtime/routes/conversation-management-routes.ts +54 -0
- package/src/runtime/routes/conversation-query-routes.ts +79 -4
- package/src/runtime/routes/gateway-log-routes.ts +14 -64
- package/src/runtime/routes/home-feed-routes.ts +10 -0
- package/src/runtime/routes/identity-intro-cache.ts +1 -1
- package/src/runtime/routes/identity-routes.ts +76 -20
- package/src/runtime/routes/inbound-message-handler.ts +0 -36
- package/src/runtime/routes/log-export-routes.ts +143 -96
- package/src/runtime/routes/plugins-routes.ts +380 -0
- package/src/runtime/routes/redact-staged-export.ts +259 -0
- package/src/runtime/routes/schedule-routes.ts +19 -2
- package/src/runtime/routes/trust-rules-routes.ts +14 -67
- package/src/schedule/recurrence-engine.ts +34 -0
- package/src/schedule/scheduler.ts +1 -0
- package/src/security/redact-json.ts +61 -0
- package/src/skills/validate-input.ts +41 -1
- package/src/subagent/types.ts +26 -1
- package/src/telemetry/types.ts +15 -1
- package/src/telemetry/usage-telemetry-reporter.test.ts +6 -1
- package/src/telemetry/usage-telemetry-reporter.ts +1 -0
- package/src/tools/apps/executors.ts +1 -1
- package/src/tools/skills/skill-tool-factory.ts +19 -8
- package/src/tools/tool-approval-handler.ts +31 -0
- package/src/usage/types.ts +8 -1
- package/src/util/platform.ts +16 -0
- package/src/watcher/engine.ts +1 -0
- package/src/workspace/adaptive-thinking-repair.ts +113 -0
- package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +70 -67
- package/src/workspace/migrations/098-remove-stale-updates-bulletin-file.ts +31 -0
- package/src/workspace/migrations/099-disable-cache-one-shot-callsites.ts +81 -0
- package/src/workspace/migrations/100-upgrade-quality-profile-to-fable-5.ts +86 -0
- package/src/workspace/migrations/101-upgrade-balanced-economy-to-minimax-m3.ts +70 -0
- package/src/workspace/migrations/registry.ts +8 -0
- package/src/__tests__/config-loader-quarantine-bulletin.test.ts +0 -202
- package/src/__tests__/conversation-starters-cadence.test.ts +0 -161
- package/src/__tests__/guardian-action-followup-executor.test.ts +0 -322
- package/src/__tests__/guardian-action-followup-store.test.ts +0 -373
- package/src/__tests__/guardian-action-late-reply.test.ts +0 -1083
- package/src/__tests__/update-bulletin-job.test.ts +0 -292
- package/src/config/schemas/updates.ts +0 -14
- package/src/memory/__tests__/compaction-log-writer-clickhouse.test.ts +0 -227
- package/src/memory/conversation-starters-cadence.ts +0 -78
- package/src/prompts/update-bulletin-job.ts +0 -180
- package/src/runtime/guardian-action-followup-executor.ts +0 -306
|
@@ -9,11 +9,13 @@
|
|
|
9
9
|
|
|
10
10
|
import { spawnSync } from "node:child_process";
|
|
11
11
|
import {
|
|
12
|
+
chmodSync,
|
|
12
13
|
mkdirSync,
|
|
13
14
|
mkdtempSync,
|
|
14
15
|
readdirSync,
|
|
15
16
|
readFileSync,
|
|
16
17
|
rmSync,
|
|
18
|
+
statSync,
|
|
17
19
|
writeFileSync,
|
|
18
20
|
} from "node:fs";
|
|
19
21
|
import { tmpdir } from "node:os";
|
|
@@ -36,9 +38,27 @@ mock.module("../util/secure-keys.js", () => ({
|
|
|
36
38
|
getSecureKeyAsync: async () => undefined,
|
|
37
39
|
}));
|
|
38
40
|
|
|
41
|
+
import { getDb } from "../memory/db-connection.js";
|
|
39
42
|
import { initializeDb } from "../memory/db-init.js";
|
|
43
|
+
import {
|
|
44
|
+
conversations,
|
|
45
|
+
llmRequestLogs,
|
|
46
|
+
toolInvocations,
|
|
47
|
+
} from "../memory/schema.js";
|
|
40
48
|
import { RouteError } from "../runtime/routes/errors.js";
|
|
41
|
-
import {
|
|
49
|
+
import {
|
|
50
|
+
MAX_EXPORT_LLM_REQUEST_LOG_ROWS,
|
|
51
|
+
ROUTES,
|
|
52
|
+
} from "../runtime/routes/log-export-routes.js";
|
|
53
|
+
import {
|
|
54
|
+
MAX_SWEEP_FILE_BYTES,
|
|
55
|
+
OVERSIZED_FILE_NOTE,
|
|
56
|
+
redactStagedExportFiles,
|
|
57
|
+
} from "../runtime/routes/redact-staged-export.js";
|
|
58
|
+
import {
|
|
59
|
+
OPENAI_PROJECT_KEY_REDACTION_MARKER,
|
|
60
|
+
SYNTHETIC_OPENAI_PROJECT_KEY,
|
|
61
|
+
} from "./secret-fixtures.js";
|
|
42
62
|
|
|
43
63
|
initializeDb();
|
|
44
64
|
|
|
@@ -98,10 +118,29 @@ async function extractArchive(res: Response): Promise<string> {
|
|
|
98
118
|
// Seed test data
|
|
99
119
|
// ---------------------------------------------------------------------------
|
|
100
120
|
|
|
101
|
-
// config.json at workspace root — needed for config-snapshot
|
|
121
|
+
// config.json at workspace root — needed for config-snapshot tests. The
|
|
122
|
+
// acp.agents env value is a synthetic secret that must be redacted from the
|
|
123
|
+
// exported snapshot. Unlike the shared `SYNTHETIC_OPENAI_PROJECT_KEY`, it is
|
|
124
|
+
// deliberately below the secret scanner's 40-char minimum, so the export-time
|
|
125
|
+
// sweep cannot see it — only the config snapshot's structural env redaction
|
|
126
|
+
// can keep it out of the archive, meaning a sanitizer regression cannot be
|
|
127
|
+
// masked by the sweep.
|
|
128
|
+
const SWEEP_INVISIBLE_SYNTHETIC_KEY = "sk-proj-synthetic-test-key-000000";
|
|
102
129
|
writeFileSync(
|
|
103
130
|
join(testWorkspaceDir, "config.json"),
|
|
104
|
-
JSON.stringify({
|
|
131
|
+
JSON.stringify({
|
|
132
|
+
provider: "anthropic",
|
|
133
|
+
acp: {
|
|
134
|
+
agents: {
|
|
135
|
+
codex: {
|
|
136
|
+
env: {
|
|
137
|
+
OPENAI_API_KEY: SWEEP_INVISIBLE_SYNTHETIC_KEY,
|
|
138
|
+
PATH: "/data/.bun/bin",
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
}),
|
|
105
144
|
);
|
|
106
145
|
|
|
107
146
|
// Conversation directories — used for workspace allowlist tests
|
|
@@ -201,6 +240,26 @@ describe("POST /v1/export — tar.gz archive", () => {
|
|
|
201
240
|
rmSync(dir, { recursive: true, force: true });
|
|
202
241
|
}
|
|
203
242
|
});
|
|
243
|
+
|
|
244
|
+
test("config-snapshot.json redacts acp.agents env values", async () => {
|
|
245
|
+
const res = await callExport();
|
|
246
|
+
const dir = await extractArchive(res);
|
|
247
|
+
try {
|
|
248
|
+
const configContent = readFileSync(
|
|
249
|
+
join(dir, "config-snapshot.json"),
|
|
250
|
+
"utf-8",
|
|
251
|
+
);
|
|
252
|
+
const parsed = JSON.parse(configContent);
|
|
253
|
+
const env = parsed.acp.agents.codex.env as Record<string, string>;
|
|
254
|
+
expect(Object.keys(env).sort()).toEqual(["OPENAI_API_KEY", "PATH"]);
|
|
255
|
+
for (const value of Object.values(env)) {
|
|
256
|
+
expect(value).toBe("(set)");
|
|
257
|
+
}
|
|
258
|
+
expect(configContent).not.toContain(SWEEP_INVISIBLE_SYNTHETIC_KEY);
|
|
259
|
+
} finally {
|
|
260
|
+
rmSync(dir, { recursive: true, force: true });
|
|
261
|
+
}
|
|
262
|
+
});
|
|
204
263
|
});
|
|
205
264
|
|
|
206
265
|
describe("POST /v1/export — daemon log date filtering", () => {
|
|
@@ -403,6 +462,13 @@ describe("POST /v1/export — workspace allowlist", () => {
|
|
|
403
462
|
);
|
|
404
463
|
const content = readFileSync(messagesPath, "utf-8");
|
|
405
464
|
expect(content).toBe('{"role":"user","content":"jan 15"}\n');
|
|
465
|
+
// Clean daemon log files must also ship byte-identical — the secret
|
|
466
|
+
// sweep does no gratuitous rewrites.
|
|
467
|
+
for (const logFile of ["assistant-2025-01-10.log", "vellum.log"]) {
|
|
468
|
+
expect(readFileSync(join(dir, "daemon-logs", logFile), "utf-8")).toBe(
|
|
469
|
+
readFileSync(join(logsDir, logFile), "utf-8"),
|
|
470
|
+
);
|
|
471
|
+
}
|
|
406
472
|
} finally {
|
|
407
473
|
rmSync(dir, { recursive: true, force: true });
|
|
408
474
|
}
|
|
@@ -442,3 +508,402 @@ describe("POST /v1/export — workspace allowlist", () => {
|
|
|
442
508
|
}
|
|
443
509
|
});
|
|
444
510
|
});
|
|
511
|
+
|
|
512
|
+
// ---------------------------------------------------------------------------
|
|
513
|
+
// Export manifest row-cap truncation
|
|
514
|
+
// ---------------------------------------------------------------------------
|
|
515
|
+
|
|
516
|
+
describe("POST /v1/export — manifest truncatedSections", () => {
|
|
517
|
+
async function readManifest(
|
|
518
|
+
body: Record<string, unknown>,
|
|
519
|
+
): Promise<Record<string, unknown>> {
|
|
520
|
+
const res = await callExport(body);
|
|
521
|
+
const dir = await extractArchive(res);
|
|
522
|
+
try {
|
|
523
|
+
return JSON.parse(
|
|
524
|
+
readFileSync(join(dir, "export-manifest.json"), "utf-8"),
|
|
525
|
+
) as Record<string, unknown>;
|
|
526
|
+
} finally {
|
|
527
|
+
rmSync(dir, { recursive: true, force: true });
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
test("omits truncatedSections when no section hits its row cap", async () => {
|
|
532
|
+
const manifest = await readManifest({ full: true });
|
|
533
|
+
expect(manifest.type).toBe("full-export");
|
|
534
|
+
expect(manifest).not.toHaveProperty("truncatedSections");
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
test("surfaces truncatedSections when a full-export dump exceeds its row cap", async () => {
|
|
538
|
+
// Seed limit + 1 rows for the cheapest capped section (llm-request-logs)
|
|
539
|
+
// so capRows detects truncation without a COUNT query.
|
|
540
|
+
const db = getDb();
|
|
541
|
+
const now = Date.now();
|
|
542
|
+
db.insert(conversations)
|
|
543
|
+
.values({ id: "conv-cap-test", createdAt: now, updatedAt: now })
|
|
544
|
+
.run();
|
|
545
|
+
const rows = Array.from(
|
|
546
|
+
{ length: MAX_EXPORT_LLM_REQUEST_LOG_ROWS + 1 },
|
|
547
|
+
(_, i) => ({
|
|
548
|
+
id: `llm-log-cap-${i}`,
|
|
549
|
+
conversationId: "conv-cap-test",
|
|
550
|
+
requestPayload: "{}",
|
|
551
|
+
responsePayload: "{}",
|
|
552
|
+
createdAt: now + i,
|
|
553
|
+
}),
|
|
554
|
+
);
|
|
555
|
+
db.insert(llmRequestLogs).values(rows).run();
|
|
556
|
+
|
|
557
|
+
const manifest = await readManifest({ full: true });
|
|
558
|
+
expect(manifest.truncatedSections).toEqual(["llm-request-logs"]);
|
|
559
|
+
});
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
// ---------------------------------------------------------------------------
|
|
563
|
+
// Export-time secret sweep
|
|
564
|
+
// ---------------------------------------------------------------------------
|
|
565
|
+
|
|
566
|
+
// NOTE: these tests intentionally run after every other describe block in
|
|
567
|
+
// this file — they seed a workspace conversation and an audit DB row that
|
|
568
|
+
// contain a raw secret, which would otherwise leak into the exports made by
|
|
569
|
+
// the earlier (clean-state) tests.
|
|
570
|
+
describe("POST /v1/export — staged-file secret sweep", () => {
|
|
571
|
+
test("clean staged files are left byte-identical and report filesRedacted: 0", () => {
|
|
572
|
+
const staging = mkdtempSync(join(tmpdir(), "redact-staged-clean-"));
|
|
573
|
+
try {
|
|
574
|
+
mkdirSync(join(staging, "daemon-logs"), { recursive: true });
|
|
575
|
+
const seeded: Record<string, string> = {
|
|
576
|
+
"audit-data.json": JSON.stringify(
|
|
577
|
+
[{ id: "ti-1", toolName: "bash", input: "{}" }],
|
|
578
|
+
null,
|
|
579
|
+
2,
|
|
580
|
+
),
|
|
581
|
+
"daemon-logs/assistant-2025-01-10.log": "log entry from Jan 10\n",
|
|
582
|
+
"notes.md": "# clean notes\n",
|
|
583
|
+
};
|
|
584
|
+
for (const [rel, content] of Object.entries(seeded)) {
|
|
585
|
+
writeFileSync(join(staging, rel), content, "utf-8");
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
const result = redactStagedExportFiles(staging);
|
|
589
|
+
|
|
590
|
+
expect(result).toEqual({
|
|
591
|
+
filesScanned: 3,
|
|
592
|
+
filesRedacted: 0,
|
|
593
|
+
filesOmitted: 0,
|
|
594
|
+
});
|
|
595
|
+
for (const [rel, content] of Object.entries(seeded)) {
|
|
596
|
+
expect(readFileSync(join(staging, rel), "utf-8")).toBe(content);
|
|
597
|
+
}
|
|
598
|
+
} finally {
|
|
599
|
+
rmSync(staging, { recursive: true, force: true });
|
|
600
|
+
}
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
test("redacts .jsonl files line-wise, keeping each valid line parseable", () => {
|
|
604
|
+
const staging = mkdtempSync(join(tmpdir(), "redact-staged-jsonl-"));
|
|
605
|
+
try {
|
|
606
|
+
const filePath = join(staging, "conversation-filtered.jsonl");
|
|
607
|
+
const validLine = JSON.stringify({
|
|
608
|
+
msg: `token ${SYNTHETIC_OPENAI_PROJECT_KEY}`,
|
|
609
|
+
});
|
|
610
|
+
const malformedLine = `not json but carries ${SYNTHETIC_OPENAI_PROJECT_KEY}`;
|
|
611
|
+
const cleanLine = JSON.stringify({ msg: "clean" });
|
|
612
|
+
writeFileSync(
|
|
613
|
+
filePath,
|
|
614
|
+
`${validLine}\n${malformedLine}\n${cleanLine}\n`,
|
|
615
|
+
"utf-8",
|
|
616
|
+
);
|
|
617
|
+
|
|
618
|
+
const result = redactStagedExportFiles(staging);
|
|
619
|
+
expect(result).toEqual({
|
|
620
|
+
filesScanned: 1,
|
|
621
|
+
filesRedacted: 1,
|
|
622
|
+
filesOmitted: 0,
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
const lines = readFileSync(filePath, "utf-8").split("\n");
|
|
626
|
+
expect(lines[0]).not.toContain(SYNTHETIC_OPENAI_PROJECT_KEY);
|
|
627
|
+
// The valid line stays valid JSON, marker stored as a string value.
|
|
628
|
+
const parsed = JSON.parse(lines[0]) as { msg: string };
|
|
629
|
+
expect(parsed.msg).toContain(OPENAI_PROJECT_KEY_REDACTION_MARKER);
|
|
630
|
+
// The unparseable line falls back to plain-text redaction.
|
|
631
|
+
expect(lines[1]).toBe(
|
|
632
|
+
`not json but carries ${OPENAI_PROJECT_KEY_REDACTION_MARKER}`,
|
|
633
|
+
);
|
|
634
|
+
// The clean line is untouched.
|
|
635
|
+
expect(lines[2]).toBe(cleanLine);
|
|
636
|
+
} finally {
|
|
637
|
+
rmSync(staging, { recursive: true, force: true });
|
|
638
|
+
}
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
test("sweeps arbitrary-extension staged files that sniff as text; leaves binary files untouched", () => {
|
|
642
|
+
const staging = mkdtempSync(join(tmpdir(), "redact-staged-sniff-"));
|
|
643
|
+
try {
|
|
644
|
+
// Conversation attachments are staged wholesale with arbitrary user
|
|
645
|
+
// extensions — sweep eligibility is decided by content sniffing, not
|
|
646
|
+
// file extension, so they must still be swept when they sniff as text.
|
|
647
|
+
const attachmentsDir = join(
|
|
648
|
+
staging,
|
|
649
|
+
"workspace",
|
|
650
|
+
"conversations",
|
|
651
|
+
"2025-01-10T00-00-00.000Z_conv-jan10",
|
|
652
|
+
"attachments",
|
|
653
|
+
);
|
|
654
|
+
mkdirSync(attachmentsDir, { recursive: true });
|
|
655
|
+
writeFileSync(
|
|
656
|
+
join(attachmentsDir, "creds.env"),
|
|
657
|
+
`OPENAI_API_KEY=${SYNTHETIC_OPENAI_PROJECT_KEY}\n`,
|
|
658
|
+
"utf-8",
|
|
659
|
+
);
|
|
660
|
+
writeFileSync(
|
|
661
|
+
join(attachmentsDir, "no-extension"),
|
|
662
|
+
`key: ${SYNTHETIC_OPENAI_PROJECT_KEY}\n`,
|
|
663
|
+
"utf-8",
|
|
664
|
+
);
|
|
665
|
+
// Binary sniff: a NUL byte in the head exempts the file from the
|
|
666
|
+
// sweep even though scanner-matching bytes appear later in it.
|
|
667
|
+
const binary = Buffer.concat([
|
|
668
|
+
Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x00, 0x01]),
|
|
669
|
+
Buffer.from(SYNTHETIC_OPENAI_PROJECT_KEY, "utf-8"),
|
|
670
|
+
]);
|
|
671
|
+
writeFileSync(join(attachmentsDir, "image.png"), binary);
|
|
672
|
+
|
|
673
|
+
const result = redactStagedExportFiles(staging);
|
|
674
|
+
|
|
675
|
+
expect(result).toEqual({
|
|
676
|
+
filesScanned: 2,
|
|
677
|
+
filesRedacted: 2,
|
|
678
|
+
filesOmitted: 0,
|
|
679
|
+
});
|
|
680
|
+
for (const file of ["creds.env", "no-extension"]) {
|
|
681
|
+
const content = readFileSync(join(attachmentsDir, file), "utf-8");
|
|
682
|
+
expect(content).toContain(OPENAI_PROJECT_KEY_REDACTION_MARKER);
|
|
683
|
+
expect(content).not.toContain(SYNTHETIC_OPENAI_PROJECT_KEY);
|
|
684
|
+
}
|
|
685
|
+
expect(readFileSync(join(attachmentsDir, "image.png"))).toEqual(binary);
|
|
686
|
+
} finally {
|
|
687
|
+
rmSync(staging, { recursive: true, force: true });
|
|
688
|
+
}
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
test("replaces oversized sweep-eligible files with an omission note (fail closed)", () => {
|
|
692
|
+
const staging = mkdtempSync(join(tmpdir(), "redact-staged-oversize-"));
|
|
693
|
+
try {
|
|
694
|
+
// One byte over the cap. The content must never ship unswept — it is
|
|
695
|
+
// replaced wholesale with the omission note. An omitted file was never
|
|
696
|
+
// actually scanned or redacted, so it only counts as omitted.
|
|
697
|
+
writeFileSync(
|
|
698
|
+
join(staging, "messages.json"),
|
|
699
|
+
Buffer.alloc(MAX_SWEEP_FILE_BYTES + 1, 0x61),
|
|
700
|
+
);
|
|
701
|
+
|
|
702
|
+
const result = redactStagedExportFiles(staging);
|
|
703
|
+
|
|
704
|
+
expect(result).toEqual({
|
|
705
|
+
filesScanned: 0,
|
|
706
|
+
filesRedacted: 0,
|
|
707
|
+
filesOmitted: 1,
|
|
708
|
+
});
|
|
709
|
+
expect(readFileSync(join(staging, "messages.json"), "utf-8")).toBe(
|
|
710
|
+
OVERSIZED_FILE_NOTE,
|
|
711
|
+
);
|
|
712
|
+
} finally {
|
|
713
|
+
rmSync(staging, { recursive: true, force: true });
|
|
714
|
+
}
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
test("redacts BOM-prefixed .json files via the JSON-aware path, preserving the BOM", () => {
|
|
718
|
+
const staging = mkdtempSync(join(tmpdir(), "redact-staged-bom-"));
|
|
719
|
+
try {
|
|
720
|
+
// A UTF-8 BOM (common in user attachments) makes JSON.parse throw, so
|
|
721
|
+
// without BOM handling the file would take the plain-text fallback and
|
|
722
|
+
// the quoted marker would corrupt the JSON string it lands in.
|
|
723
|
+
const filePath = join(staging, "attachment.json");
|
|
724
|
+
writeFileSync(
|
|
725
|
+
filePath,
|
|
726
|
+
"\uFEFF" +
|
|
727
|
+
JSON.stringify({ key: SYNTHETIC_OPENAI_PROJECT_KEY }, null, 2),
|
|
728
|
+
"utf-8",
|
|
729
|
+
);
|
|
730
|
+
|
|
731
|
+
const result = redactStagedExportFiles(staging);
|
|
732
|
+
expect(result).toEqual({
|
|
733
|
+
filesScanned: 1,
|
|
734
|
+
filesRedacted: 1,
|
|
735
|
+
filesOmitted: 0,
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
const content = readFileSync(filePath, "utf-8");
|
|
739
|
+
expect(content.startsWith("\uFEFF")).toBe(true);
|
|
740
|
+
expect(content).not.toContain(SYNTHETIC_OPENAI_PROJECT_KEY);
|
|
741
|
+
// Stripping the preserved BOM yields valid JSON with the marker stored
|
|
742
|
+
// as a proper string value.
|
|
743
|
+
const parsed = JSON.parse(content.slice(1)) as { key: string };
|
|
744
|
+
expect(parsed.key).toContain(OPENAI_PROJECT_KEY_REDACTION_MARKER);
|
|
745
|
+
} finally {
|
|
746
|
+
rmSync(staging, { recursive: true, force: true });
|
|
747
|
+
}
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
// Root bypasses file permission checks, so chmod 0o444 cannot make the
|
|
751
|
+
// file read-only — skip rather than assert a fail-closed path that cannot
|
|
752
|
+
// be exercised.
|
|
753
|
+
test.skipIf(process.getuid?.() === 0)(
|
|
754
|
+
"redacts a read-only staged file in place instead of shipping the raw secret",
|
|
755
|
+
() => {
|
|
756
|
+
const staging = mkdtempSync(join(tmpdir(), "redact-staged-readonly-"));
|
|
757
|
+
try {
|
|
758
|
+
// A read-only workspace attachment is staged with its mode bits
|
|
759
|
+
// preserved (cpSync). The sweep must still rewrite it — the chmod
|
|
760
|
+
// path — so the raw secret never reaches the tar step.
|
|
761
|
+
const readonlyPath = join(staging, "creds.env");
|
|
762
|
+
writeFileSync(
|
|
763
|
+
readonlyPath,
|
|
764
|
+
`OPENAI_API_KEY=${SYNTHETIC_OPENAI_PROJECT_KEY}\n`,
|
|
765
|
+
"utf-8",
|
|
766
|
+
);
|
|
767
|
+
chmodSync(readonlyPath, 0o444);
|
|
768
|
+
// A clean read-only file needs no rewrite, so it must keep its
|
|
769
|
+
// content AND its mode bits (no gratuitous chmod).
|
|
770
|
+
const cleanPath = join(staging, "clean.txt");
|
|
771
|
+
writeFileSync(cleanPath, "nothing secret here\n", "utf-8");
|
|
772
|
+
chmodSync(cleanPath, 0o444);
|
|
773
|
+
|
|
774
|
+
const result = redactStagedExportFiles(staging);
|
|
775
|
+
|
|
776
|
+
expect(result).toEqual({
|
|
777
|
+
filesScanned: 2,
|
|
778
|
+
filesRedacted: 1,
|
|
779
|
+
filesOmitted: 0,
|
|
780
|
+
});
|
|
781
|
+
const content = readFileSync(readonlyPath, "utf-8");
|
|
782
|
+
expect(content).not.toContain(SYNTHETIC_OPENAI_PROJECT_KEY);
|
|
783
|
+
expect(content).toContain(OPENAI_PROJECT_KEY_REDACTION_MARKER);
|
|
784
|
+
expect(readFileSync(cleanPath, "utf-8")).toBe("nothing secret here\n");
|
|
785
|
+
expect(statSync(cleanPath).mode & 0o777).toBe(0o444);
|
|
786
|
+
} finally {
|
|
787
|
+
rmSync(staging, { recursive: true, force: true });
|
|
788
|
+
}
|
|
789
|
+
},
|
|
790
|
+
);
|
|
791
|
+
|
|
792
|
+
// Root bypasses file permission checks, so chmod 0o000 cannot make the
|
|
793
|
+
// file unreadable — skip rather than assert a degraded path that cannot
|
|
794
|
+
// be exercised.
|
|
795
|
+
test.skipIf(process.getuid?.() === 0)(
|
|
796
|
+
"sweep continues past an unreadable staged file (degraded, not failed)",
|
|
797
|
+
() => {
|
|
798
|
+
const staging = mkdtempSync(join(tmpdir(), "redact-staged-unreadable-"));
|
|
799
|
+
const unreadablePath = join(staging, "unreadable.log");
|
|
800
|
+
try {
|
|
801
|
+
writeFileSync(
|
|
802
|
+
join(staging, "readable.log"),
|
|
803
|
+
`key ${SYNTHETIC_OPENAI_PROJECT_KEY}\n`,
|
|
804
|
+
"utf-8",
|
|
805
|
+
);
|
|
806
|
+
writeFileSync(unreadablePath, "any content\n", "utf-8");
|
|
807
|
+
chmodSync(unreadablePath, 0o000);
|
|
808
|
+
|
|
809
|
+
// Must return normally: the unreadable file is logged and skipped,
|
|
810
|
+
// and every other staged file is still swept.
|
|
811
|
+
const result = redactStagedExportFiles(staging);
|
|
812
|
+
|
|
813
|
+
expect(result).toEqual({
|
|
814
|
+
filesScanned: 1,
|
|
815
|
+
filesRedacted: 1,
|
|
816
|
+
filesOmitted: 0,
|
|
817
|
+
});
|
|
818
|
+
const content = readFileSync(join(staging, "readable.log"), "utf-8");
|
|
819
|
+
expect(content).toContain(OPENAI_PROJECT_KEY_REDACTION_MARKER);
|
|
820
|
+
expect(content).not.toContain(SYNTHETIC_OPENAI_PROJECT_KEY);
|
|
821
|
+
} finally {
|
|
822
|
+
chmodSync(unreadablePath, 0o600);
|
|
823
|
+
rmSync(staging, { recursive: true, force: true });
|
|
824
|
+
}
|
|
825
|
+
},
|
|
826
|
+
);
|
|
827
|
+
|
|
828
|
+
test("redacts raw keys from workspace conversation files in the archive", async () => {
|
|
829
|
+
seedConversation(
|
|
830
|
+
"2025-01-30T00-00-00.000Z_conv-secret",
|
|
831
|
+
JSON.stringify({
|
|
832
|
+
role: "user",
|
|
833
|
+
content: `export OPENAI_API_KEY="${SYNTHETIC_OPENAI_PROJECT_KEY}"`,
|
|
834
|
+
}) + "\n",
|
|
835
|
+
);
|
|
836
|
+
|
|
837
|
+
const res = await callExport();
|
|
838
|
+
const dir = await extractArchive(res);
|
|
839
|
+
try {
|
|
840
|
+
const content = readFileSync(
|
|
841
|
+
join(
|
|
842
|
+
dir,
|
|
843
|
+
"workspace",
|
|
844
|
+
"conversations",
|
|
845
|
+
"2025-01-30T00-00-00.000Z_conv-secret",
|
|
846
|
+
"messages.jsonl",
|
|
847
|
+
),
|
|
848
|
+
"utf-8",
|
|
849
|
+
);
|
|
850
|
+
expect(content).not.toContain(SYNTHETIC_OPENAI_PROJECT_KEY);
|
|
851
|
+
// Line-wise JSON-aware redaction: every redacted line must still
|
|
852
|
+
// JSON.parse, with the marker stored as a proper JSON string value
|
|
853
|
+
// (its quotes are escaped on serialization).
|
|
854
|
+
const lines = content.split("\n").filter((line) => line.trim() !== "");
|
|
855
|
+
expect(lines.length).toBeGreaterThan(0);
|
|
856
|
+
const parsedLines = lines.map(
|
|
857
|
+
(line) => JSON.parse(line) as { content?: string },
|
|
858
|
+
);
|
|
859
|
+
expect(
|
|
860
|
+
parsedLines.some((record) =>
|
|
861
|
+
record.content?.includes(OPENAI_PROJECT_KEY_REDACTION_MARKER),
|
|
862
|
+
),
|
|
863
|
+
).toBe(true);
|
|
864
|
+
} finally {
|
|
865
|
+
rmSync(dir, { recursive: true, force: true });
|
|
866
|
+
}
|
|
867
|
+
});
|
|
868
|
+
|
|
869
|
+
test("redacts legacy audit rows and keeps audit-data.json valid JSON", async () => {
|
|
870
|
+
// Simulate a row written before write-time input redaction shipped: the
|
|
871
|
+
// raw key sits in the persisted `input` column, where no structural
|
|
872
|
+
// sanitizer can retroactively fix it.
|
|
873
|
+
const db = getDb();
|
|
874
|
+
const now = Date.now();
|
|
875
|
+
db.insert(conversations)
|
|
876
|
+
.values({ id: "conv-legacy-audit", createdAt: now, updatedAt: now })
|
|
877
|
+
.run();
|
|
878
|
+
db.insert(toolInvocations)
|
|
879
|
+
.values({
|
|
880
|
+
id: "ti-legacy-audit",
|
|
881
|
+
conversationId: "conv-legacy-audit",
|
|
882
|
+
toolName: "bash",
|
|
883
|
+
input: JSON.stringify({
|
|
884
|
+
command: `export OPENAI_API_KEY="${SYNTHETIC_OPENAI_PROJECT_KEY}"`,
|
|
885
|
+
}),
|
|
886
|
+
result: "{}",
|
|
887
|
+
decision: "allow",
|
|
888
|
+
riskLevel: "low",
|
|
889
|
+
durationMs: 5,
|
|
890
|
+
createdAt: now,
|
|
891
|
+
})
|
|
892
|
+
.run();
|
|
893
|
+
|
|
894
|
+
const res = await callExport();
|
|
895
|
+
const dir = await extractArchive(res);
|
|
896
|
+
try {
|
|
897
|
+
const content = readFileSync(join(dir, "audit-data.json"), "utf-8");
|
|
898
|
+
expect(content).not.toContain(SYNTHETIC_OPENAI_PROJECT_KEY);
|
|
899
|
+
// The sweep must keep the file parseable — redaction goes through a
|
|
900
|
+
// JSON-aware path rather than splicing quoted markers into raw JSON.
|
|
901
|
+
const rows = JSON.parse(content) as Array<{ id: string; input: string }>;
|
|
902
|
+
const row = rows.find((r) => r.id === "ti-legacy-audit");
|
|
903
|
+
expect(row).toBeDefined();
|
|
904
|
+
expect(row!.input).toContain(OPENAI_PROJECT_KEY_REDACTION_MARKER);
|
|
905
|
+
} finally {
|
|
906
|
+
rmSync(dir, { recursive: true, force: true });
|
|
907
|
+
}
|
|
908
|
+
});
|
|
909
|
+
});
|
|
@@ -74,6 +74,12 @@ function seedThreads(body: string): void {
|
|
|
74
74
|
writeFileSync(path, body, "utf-8");
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
function seedBuffer(body: string): void {
|
|
78
|
+
const path = getWorkspacePromptPath("memory/buffer.md");
|
|
79
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
80
|
+
writeFileSync(path, body, "utf-8");
|
|
81
|
+
}
|
|
82
|
+
|
|
77
83
|
function clearV2StaticFiles(): void {
|
|
78
84
|
for (const file of [
|
|
79
85
|
"memory/essentials.md",
|
|
@@ -116,6 +122,22 @@ describe("memory-v2-static injector", () => {
|
|
|
116
122
|
);
|
|
117
123
|
});
|
|
118
124
|
|
|
125
|
+
test("suppresses the Buffer section on memoryV2Consolidation turns", async () => {
|
|
126
|
+
seedEssentials("Alice prefers VS Code.");
|
|
127
|
+
seedBuffer("- [Apr 27, 9:00 AM] backlog entry");
|
|
128
|
+
|
|
129
|
+
const consolidationBlock = await memoryV2StaticInjector.produce(
|
|
130
|
+
makeContext({ callSite: "memoryV2Consolidation" }),
|
|
131
|
+
);
|
|
132
|
+
expect(consolidationBlock).not.toBeNull();
|
|
133
|
+
expect(consolidationBlock!.text).toContain("## Essentials");
|
|
134
|
+
expect(consolidationBlock!.text).not.toContain("## Buffer");
|
|
135
|
+
|
|
136
|
+
// Other call sites (and untagged turns) keep the buffer section.
|
|
137
|
+
const mainBlock = await memoryV2StaticInjector.produce(makeContext());
|
|
138
|
+
expect(mainBlock!.text).toContain("## Buffer");
|
|
139
|
+
});
|
|
140
|
+
|
|
119
141
|
test("escapes inner </info> closing tags so the wrapper cannot be broken out of", async () => {
|
|
120
142
|
seedEssentials("Text with </info> embedded.");
|
|
121
143
|
const ctx = makeContext();
|
|
@@ -20,7 +20,7 @@ describe("model intents", () => {
|
|
|
20
20
|
"claude-haiku-4-5-20251001",
|
|
21
21
|
);
|
|
22
22
|
expect(resolveModelIntent("anthropic", "quality-optimized")).toBe(
|
|
23
|
-
"claude-
|
|
23
|
+
"claude-fable-5",
|
|
24
24
|
);
|
|
25
25
|
expect(resolveModelIntent("anthropic", "vision-optimized")).toBe(
|
|
26
26
|
"claude-opus-4-6",
|
|
@@ -25,8 +25,10 @@ import { describe, expect, mock, test } from "bun:test";
|
|
|
25
25
|
|
|
26
26
|
import { Command } from "commander";
|
|
27
27
|
|
|
28
|
-
let mockPlatformClientCreate: () => Promise<Record<
|
|
29
|
-
|
|
28
|
+
let mockPlatformClientCreate: () => Promise<Record<
|
|
29
|
+
string,
|
|
30
|
+
unknown
|
|
31
|
+
> | null> = async () => null;
|
|
30
32
|
|
|
31
33
|
mock.module("../platform/client.js", () => ({
|
|
32
34
|
VellumPlatformClient: {
|
|
@@ -49,18 +51,27 @@ mock.module("../config/loader.js", () => ({
|
|
|
49
51
|
mergeDefaultWorkspaceConfig: () => {},
|
|
50
52
|
getNestedValue: () => undefined,
|
|
51
53
|
setNestedValue: () => {},
|
|
52
|
-
|
|
54
|
+
_writeQuarantineNotice: () => {},
|
|
53
55
|
API_KEY_PROVIDERS: ["anthropic", "openai", "gemini"],
|
|
54
56
|
}));
|
|
55
57
|
|
|
56
58
|
mock.module("../util/logger.js", () => ({
|
|
57
|
-
getLogger: () => ({
|
|
58
|
-
|
|
59
|
+
getLogger: () => ({
|
|
60
|
+
info: () => {},
|
|
61
|
+
warn: () => {},
|
|
62
|
+
error: () => {},
|
|
63
|
+
debug: () => {},
|
|
64
|
+
}),
|
|
65
|
+
getCliLogger: () => ({
|
|
66
|
+
info: () => {},
|
|
67
|
+
warn: () => {},
|
|
68
|
+
error: () => {},
|
|
69
|
+
debug: () => {},
|
|
70
|
+
}),
|
|
59
71
|
}));
|
|
60
72
|
|
|
61
|
-
const { requirePlatformConnection } =
|
|
62
|
-
"../cli/commands/oauth/shared.js"
|
|
63
|
-
);
|
|
73
|
+
const { requirePlatformConnection } =
|
|
74
|
+
await import("../cli/commands/oauth/shared.js");
|
|
64
75
|
|
|
65
76
|
// ---------------------------------------------------------------------------
|
|
66
77
|
// requirePlatformConnection
|
|
@@ -1364,6 +1364,40 @@ describe("custom baseURL initialization", () => {
|
|
|
1364
1364
|
timeout: DEFAULT_SDK_TIMEOUT_MS,
|
|
1365
1365
|
});
|
|
1366
1366
|
});
|
|
1367
|
+
|
|
1368
|
+
test("MinimaxProvider sends reasoning_split so reasoning never leaks into content as <think> tags", async () => {
|
|
1369
|
+
fakeChunks = [textChunk("hello", "stop"), usageChunk(10, 5)];
|
|
1370
|
+
const provider = new MinimaxProvider("mm-user-key", "MiniMax-M3");
|
|
1371
|
+
|
|
1372
|
+
await provider.sendMessage([userMsg("hi")]);
|
|
1373
|
+
|
|
1374
|
+
expect(lastCreateParams!.reasoning_split).toBe(true);
|
|
1375
|
+
});
|
|
1376
|
+
|
|
1377
|
+
test("MinimaxProvider replays prior assistant thinking via reasoning_content", async () => {
|
|
1378
|
+
fakeChunks = [textChunk("done", "stop"), usageChunk(10, 5)];
|
|
1379
|
+
const provider = new MinimaxProvider("mm-user-key", "MiniMax-M3");
|
|
1380
|
+
|
|
1381
|
+
await provider.sendMessage([
|
|
1382
|
+
userMsg("question"),
|
|
1383
|
+
{
|
|
1384
|
+
role: "assistant",
|
|
1385
|
+
content: [
|
|
1386
|
+
{ type: "thinking", thinking: "prior reasoning", signature: "" },
|
|
1387
|
+
{ type: "text", text: "prior answer" },
|
|
1388
|
+
],
|
|
1389
|
+
},
|
|
1390
|
+
userMsg("follow-up"),
|
|
1391
|
+
]);
|
|
1392
|
+
|
|
1393
|
+
const sent = lastCreateParams!.messages as Array<Record<string, unknown>>;
|
|
1394
|
+
const assistantTurn = sent.find((m) => m.role === "assistant");
|
|
1395
|
+
expect(assistantTurn).toEqual({
|
|
1396
|
+
role: "assistant",
|
|
1397
|
+
content: "prior answer",
|
|
1398
|
+
reasoning_content: "prior reasoning",
|
|
1399
|
+
});
|
|
1400
|
+
});
|
|
1367
1401
|
});
|
|
1368
1402
|
|
|
1369
1403
|
// ---------------------------------------------------------------------------
|