@vellumai/assistant 0.6.0 → 0.6.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/AGENTS.md +4 -0
- package/ARCHITECTURE.md +68 -15
- package/Dockerfile +2 -2
- package/bun.lock +6 -2
- package/docker-entrypoint.sh +32 -1
- package/docs/architecture/integrations.md +1 -1
- package/docs/architecture/memory.md +21 -24
- package/openapi.yaml +538 -3
- package/package.json +5 -1
- package/src/__tests__/anthropic-provider.test.ts +160 -95
- package/src/__tests__/app-dir-path-guard.test.ts +1 -0
- package/src/__tests__/app-executors.test.ts +47 -1
- package/src/__tests__/app-source-watcher.test.ts +159 -0
- package/src/__tests__/checker.test.ts +38 -6
- package/src/__tests__/config-schema.test.ts +5 -0
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +4 -6
- package/src/__tests__/conversation-agent-loop.test.ts +4 -51
- package/src/__tests__/conversation-history-web-search.test.ts +1 -1
- package/src/__tests__/conversation-runtime-assembly.test.ts +653 -832
- package/src/__tests__/conversation-runtime-workspace.test.ts +1 -93
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +17 -4
- package/src/__tests__/conversation-wipe.test.ts +2 -6
- package/src/__tests__/conversation-workspace-cache-state.test.ts +6 -12
- package/src/__tests__/conversation-workspace-injection.test.ts +25 -26
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -1
- package/src/__tests__/copy-composer-tc-templates.test.ts +335 -0
- package/src/__tests__/date-context.test.ts +76 -210
- package/src/__tests__/db-schedule-syntax-migration.test.ts +16 -1
- package/src/__tests__/file-list-tool.test.ts +219 -0
- package/src/__tests__/first-greeting.test.ts +1 -1
- package/src/__tests__/heartbeat-service.test.ts +180 -3
- package/src/__tests__/identity-routes.test.ts +328 -0
- package/src/__tests__/injection-block.test.ts +24 -0
- package/src/__tests__/install-skill-routing.test.ts +7 -6
- package/src/__tests__/jobs-store-qdrant-breaker.test.ts +15 -14
- package/src/__tests__/list-messages-tool-merge.test.ts +300 -0
- package/src/__tests__/llm-context-normalization.test.ts +18 -18
- package/src/__tests__/llm-context-route-provider.test.ts +101 -0
- package/src/__tests__/llm-request-log-turn-query.test.ts +162 -0
- package/src/__tests__/log-export-workspace.test.ts +72 -105
- package/src/__tests__/mcp-abort-signal.test.ts +5 -0
- package/src/__tests__/mcp-client-auth.test.ts +5 -0
- package/src/__tests__/memory-recall-log-store.test.ts +132 -0
- package/src/__tests__/migration-export-streaming.test.ts +304 -0
- package/src/__tests__/migration-import-commit-http.test.ts +11 -10
- package/src/__tests__/mock-fetch.ts +87 -0
- package/src/__tests__/notification-decision-recipient-context.test.ts +282 -0
- package/src/__tests__/onboarding-template-contract.test.ts +62 -14
- package/src/__tests__/parser.test.ts +32 -0
- package/src/__tests__/permission-checker-host-gate.test.ts +452 -0
- package/src/__tests__/permission-controls-v2-flag.test.ts +55 -0
- package/src/__tests__/permission-mode-sse.test.ts +418 -0
- package/src/__tests__/permission-mode-store.test.ts +277 -0
- package/src/__tests__/permission-mode.test.ts +101 -0
- package/src/__tests__/platform-bash-auto-approve.test.ts +359 -0
- package/src/__tests__/profiler-routes.test.ts +502 -0
- package/src/__tests__/profiler-run-store.test.ts +441 -0
- package/src/__tests__/proxy-approval-callback.test.ts +4 -75
- package/src/__tests__/registry.test.ts +1 -1
- package/src/__tests__/sandbox-host-parity.test.ts +5 -4
- package/src/__tests__/scheduler-reuse-conversation.test.ts +368 -0
- package/src/__tests__/scrub-corrupted-image-attachments.test.ts +278 -0
- package/src/__tests__/search-skills-unified.test.ts +4 -3
- package/src/__tests__/send-endpoint-busy.test.ts +42 -3
- package/src/__tests__/set-permission-mode.test.ts +274 -0
- package/src/__tests__/skill-load-feature-flag.test.ts +12 -0
- package/src/__tests__/skill-memory.test.ts +2 -783
- package/src/__tests__/strip-memory-injections.test.ts +187 -0
- package/src/__tests__/subagent-detail.test.ts +84 -0
- package/src/__tests__/subagent-disposal.test.ts +308 -0
- package/src/__tests__/subagent-manager-notify.test.ts +19 -10
- package/src/__tests__/subagent-notify-parent.test.ts +390 -0
- package/src/__tests__/subagent-role-registry.test.ts +108 -0
- package/src/__tests__/subagent-tool-filtering.test.ts +71 -0
- package/src/__tests__/subagent-tools.test.ts +464 -4
- package/src/__tests__/system-prompt-ask-mode.test.ts +139 -0
- package/src/__tests__/task-memory-cleanup.test.ts +12 -12
- package/src/__tests__/terminal-tools.test.ts +17 -27
- package/src/__tests__/test-preload.ts +4 -0
- package/src/__tests__/tool-executor.test.ts +4 -26
- package/src/__tests__/tool-side-effects-slack-dm.test.ts +1 -0
- package/src/__tests__/top-level-renderer.test.ts +10 -13
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +116 -2
- package/src/__tests__/workspace-migration-028-recover-conversations-from-disk-view.test.ts +387 -0
- package/src/agent/loop.ts +6 -0
- package/src/approvals/guardian-request-resolvers.ts +24 -0
- package/src/avatar/traits-png-sync.ts +3 -3
- package/src/cli/__tests__/run-assistant-command.ts +29 -0
- package/src/cli/commands/__tests__/email-download.test.ts +245 -0
- package/src/cli/commands/__tests__/email-list.test.ts +192 -0
- package/src/cli/commands/__tests__/email-register.test.ts +186 -0
- package/src/cli/commands/__tests__/email-send.test.ts +291 -0
- package/src/cli/commands/__tests__/email-status.test.ts +181 -0
- package/src/cli/commands/__tests__/email-unregister.test.ts +139 -0
- package/src/cli/commands/__tests__/routes.test.ts +562 -0
- package/src/cli/commands/conversations.ts +1 -8
- package/src/cli/commands/email.ts +584 -835
- package/src/cli/commands/memory.ts +1 -34
- package/src/cli/commands/notifications.ts +7 -2
- package/src/cli/commands/oauth/connect.ts +14 -5
- package/src/cli/commands/routes.ts +396 -0
- package/src/cli/commands/skills.ts +130 -20
- package/src/cli/program.ts +2 -0
- package/src/cli.ts +1 -120
- package/src/config/bundled-skills/app-builder/SKILL.md +4 -1
- package/src/config/bundled-skills/gmail/SKILL.md +2 -2
- package/src/config/bundled-skills/messaging/SKILL.md +7 -0
- package/src/config/bundled-skills/schedule/SKILL.md +22 -2
- package/src/config/bundled-skills/schedule/TOOLS.json +8 -0
- package/src/config/bundled-skills/settings/tools/avatar-get.ts +3 -13
- package/src/config/bundled-skills/settings/tools/avatar-remove.ts +2 -4
- package/src/config/bundled-skills/settings/tools/avatar-update.ts +5 -2
- package/src/config/bundled-skills/slack/SKILL.md +2 -0
- package/src/config/bundled-skills/subagent/SKILL.md +43 -3
- package/src/config/bundled-skills/subagent/TOOLS.json +29 -4
- package/src/config/env-registry.ts +63 -0
- package/src/config/feature-flag-registry.json +17 -1
- package/src/config/schema.ts +8 -0
- package/src/config/schemas/filing.ts +51 -0
- package/src/config/schemas/heartbeat.ts +15 -12
- package/src/config/schemas/memory-lifecycle.ts +12 -0
- package/src/config/schemas/security.ts +14 -0
- package/src/daemon/app-source-watcher.ts +93 -0
- package/src/daemon/config-watcher.ts +79 -1
- package/src/daemon/conversation-agent-loop-handlers.ts +20 -0
- package/src/daemon/conversation-agent-loop.ts +158 -65
- package/src/daemon/conversation-history.ts +4 -19
- package/src/daemon/conversation-lifecycle.ts +8 -14
- package/src/daemon/conversation-process.ts +13 -7
- package/src/daemon/conversation-runtime-assembly.ts +300 -306
- package/src/daemon/conversation-tool-setup.ts +44 -14
- package/src/daemon/conversation-workspace.ts +1 -2
- package/src/daemon/conversation.ts +18 -0
- package/src/daemon/date-context.ts +26 -53
- package/src/daemon/first-greeting.ts +1 -1
- package/src/daemon/handlers/conversations.ts +4 -7
- package/src/daemon/handlers/shared.test.ts +143 -0
- package/src/daemon/handlers/shared.ts +63 -5
- package/src/daemon/handlers/skills.ts +11 -18
- package/src/daemon/lifecycle.ts +199 -157
- package/src/daemon/message-types/conversations.ts +25 -6
- package/src/daemon/message-types/messages.ts +9 -1
- package/src/daemon/message-types/schedules.ts +1 -0
- package/src/daemon/message-types/settings.ts +6 -0
- package/src/daemon/profiler-run-store.ts +557 -0
- package/src/daemon/server.ts +89 -9
- package/src/daemon/shutdown-handlers.ts +5 -0
- package/src/daemon/tool-side-effects.ts +23 -3
- package/src/export/transcript-formatter.ts +148 -0
- package/src/filing/filing-service.ts +228 -0
- package/src/heartbeat/heartbeat-service.ts +96 -7
- package/src/mcp/client.ts +6 -0
- package/src/mcp/mcp-oauth-provider.ts +149 -27
- package/src/memory/admin.ts +33 -32
- package/src/memory/app-store.ts +69 -0
- package/src/memory/conversation-bootstrap.ts +1 -1
- package/src/memory/conversation-crud.ts +136 -107
- package/src/memory/conversation-group-migration.ts +1 -1
- package/src/memory/conversation-queries.ts +58 -12
- package/src/memory/conversation-title-service.ts +1 -0
- package/src/memory/db-init.ts +182 -376
- package/src/memory/graph/bootstrap.ts +75 -66
- package/src/memory/graph/capability-seed.ts +167 -15
- package/src/memory/graph/consolidation.ts +38 -4
- package/src/memory/graph/conversation-graph-memory.ts +133 -104
- package/src/memory/graph/extraction-job.ts +9 -4
- package/src/memory/graph/extraction.ts +66 -23
- package/src/memory/graph/graph-memory-state-store.ts +37 -0
- package/src/memory/graph/graph-search.ts +29 -15
- package/src/memory/graph/injection.ts +38 -8
- package/src/memory/graph/inspect.ts +12 -3
- package/src/memory/graph/retriever.ts +365 -262
- package/src/memory/graph/store.test.ts +48 -0
- package/src/memory/graph/store.ts +150 -11
- package/src/memory/graph/tool-handlers.ts +84 -209
- package/src/memory/graph/tools.ts +8 -52
- package/src/memory/graph/types.ts +24 -0
- package/src/memory/job-handlers/cleanup.ts +44 -1
- package/src/memory/jobs-store.ts +70 -60
- package/src/memory/jobs-worker.ts +44 -28
- package/src/memory/llm-request-log-store.ts +96 -12
- package/src/memory/memory-recall-log-store.ts +49 -5
- package/src/memory/migrations/203-drop-memory-items-tables.ts +33 -1
- package/src/memory/migrations/206-memory-graph-node-edits.ts +19 -0
- package/src/memory/migrations/206-scrub-corrupted-image-attachments.ts +131 -0
- package/src/memory/migrations/207-conversation-graph-memory-state.ts +20 -0
- package/src/memory/migrations/208-conversations-last-message-at.ts +35 -0
- package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +85 -0
- package/src/memory/migrations/210-schedule-reuse-conversation.ts +13 -0
- package/src/memory/migrations/211-memory-recall-logs-query-context.ts +21 -0
- package/src/memory/migrations/212-llm-request-logs-created-at-index.ts +19 -0
- package/src/memory/migrations/index.ts +8 -0
- package/src/memory/migrations/registry.ts +8 -0
- package/src/memory/schema/conversations.ts +14 -0
- package/src/memory/schema/infrastructure.ts +8 -1
- package/src/memory/schema/memory-core.ts +0 -51
- package/src/memory/schema/memory-graph.ts +15 -0
- package/src/memory/task-memory-cleanup.ts +30 -11
- package/src/notifications/copy-composer.ts +86 -0
- package/src/notifications/decision-engine.ts +35 -0
- package/src/permissions/checker.ts +12 -1
- package/src/permissions/permission-mode-store.ts +180 -0
- package/src/permissions/permission-mode.ts +31 -0
- package/src/permissions/workspace-policy.ts +9 -0
- package/src/prompts/system-prompt.ts +59 -7
- package/src/prompts/templates/BOOTSTRAP-REFERENCE.md +100 -0
- package/src/prompts/templates/BOOTSTRAP.md +70 -165
- package/src/prompts/templates/HEARTBEAT.md +3 -1
- package/src/prompts/templates/SOUL.md +25 -4
- package/src/prompts/templates/UPDATES.md +8 -0
- package/src/providers/anthropic/client.ts +107 -219
- package/src/runtime/auth/route-policy.ts +23 -0
- package/src/runtime/http-server.ts +32 -2
- package/src/runtime/http-types.ts +12 -1
- package/src/runtime/migrations/vbundle-builder.ts +389 -3
- package/src/runtime/migrations/vbundle-importer.ts +8 -6
- package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +378 -0
- package/src/runtime/routes/app-management-routes.ts +1 -11
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +26 -0
- package/src/runtime/routes/archive-utils.ts +29 -0
- package/src/runtime/routes/avatar-routes.ts +2 -9
- package/src/runtime/routes/btw-routes.ts +14 -1
- package/src/runtime/routes/conversation-analysis-routes.ts +173 -0
- package/src/runtime/routes/conversation-management-routes.ts +1 -14
- package/src/runtime/routes/conversation-query-routes.ts +49 -3
- package/src/runtime/routes/conversation-routes.ts +264 -44
- package/src/runtime/routes/heartbeat-routes.ts +4 -10
- package/src/runtime/routes/identity-routes.ts +53 -18
- package/src/runtime/routes/llm-context-normalization.ts +14 -10
- package/src/runtime/routes/log-export-routes.ts +23 -275
- package/src/runtime/routes/memory-item-routes.test.ts +168 -233
- package/src/runtime/routes/migration-routes.ts +18 -7
- package/src/runtime/routes/profiler-routes.ts +350 -0
- package/src/runtime/routes/schedule-routes.ts +27 -12
- package/src/runtime/routes/settings-routes.ts +95 -8
- package/src/runtime/routes/subagents-routes.ts +28 -7
- package/src/runtime/routes/user-route-dispatcher.ts +223 -0
- package/src/runtime/routes/user-routes.ts +41 -0
- package/src/runtime/routes/workspace-routes.ts +0 -1
- package/src/schedule/schedule-store.ts +30 -0
- package/src/schedule/scheduler.ts +45 -18
- package/src/skills/catalog-install.ts +10 -2
- package/src/skills/managed-store.ts +2 -2
- package/src/skills/skill-memory.ts +1 -293
- package/src/subagent/index.ts +13 -3
- package/src/subagent/manager.ts +308 -29
- package/src/subagent/types.ts +68 -0
- package/src/tasks/task-runner.ts +4 -4
- package/src/tools/apps/executors.ts +29 -4
- package/src/tools/filesystem/list.ts +93 -0
- package/src/tools/permission-checker.ts +78 -0
- package/src/tools/registry.ts +4 -0
- package/src/tools/schedule/create.ts +3 -0
- package/src/tools/schedule/list.ts +1 -0
- package/src/tools/schedule/update.ts +6 -0
- package/src/tools/shared/filesystem/errors.ts +5 -0
- package/src/tools/shared/filesystem/file-ops-service.ts +90 -2
- package/src/tools/shared/filesystem/types.ts +17 -0
- package/src/tools/shared/shell-output.ts +31 -2
- package/src/tools/subagent/abort.ts +12 -2
- package/src/tools/subagent/message.ts +9 -2
- package/src/tools/subagent/notify-parent.ts +79 -0
- package/src/tools/subagent/read.ts +29 -8
- package/src/tools/subagent/resolve.ts +21 -0
- package/src/tools/subagent/spawn.ts +2 -0
- package/src/tools/subagent/status.ts +11 -1
- package/src/tools/system/avatar-generator.ts +3 -3
- package/src/tools/system/register.ts +23 -0
- package/src/tools/system/set-permission-mode.ts +103 -0
- package/src/tools/terminal/parser.ts +30 -5
- package/src/tools/terminal/safe-env.ts +16 -1
- package/src/tools/tool-manifest.ts +6 -0
- package/src/tools/types.ts +2 -0
- package/src/util/logger.ts +1 -1
- package/src/util/platform.ts +50 -17
- package/src/workspace/migrations/023-move-config-files-to-workspace.ts +2 -2
- package/src/workspace/migrations/024-move-runtime-files-to-workspace.ts +2 -2
- package/src/workspace/migrations/028-recover-conversations-from-disk-view.ts +270 -0
- package/src/workspace/migrations/029-seed-pkb.ts +84 -0
- package/src/workspace/migrations/registry.ts +4 -0
- package/src/workspace/top-level-renderer.ts +5 -9
- package/src/__tests__/cli-memory.test.ts +0 -377
- package/src/__tests__/clipboard.test.ts +0 -88
- package/src/cli/cli-memory.ts +0 -179
- package/src/util/clipboard.ts +0 -34
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import {
|
|
2
|
+
mkdirSync,
|
|
3
|
+
mkdtempSync,
|
|
4
|
+
realpathSync,
|
|
5
|
+
rmSync,
|
|
6
|
+
writeFileSync,
|
|
7
|
+
} from "node:fs";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
isToolActiveForContext,
|
|
14
|
+
type SkillProjectionContext,
|
|
15
|
+
SUBAGENT_ONLY_TOOL_NAMES,
|
|
16
|
+
} from "../daemon/conversation-tool-setup.js";
|
|
17
|
+
import { fileListTool } from "../tools/filesystem/list.js";
|
|
18
|
+
import {
|
|
19
|
+
FileSystemOps,
|
|
20
|
+
type PathPolicy,
|
|
21
|
+
} from "../tools/shared/filesystem/file-ops-service.js";
|
|
22
|
+
import { sandboxPolicy } from "../tools/shared/filesystem/path-policy.js";
|
|
23
|
+
import type { ToolContext } from "../tools/types.js";
|
|
24
|
+
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Helpers
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
const testDirs: string[] = [];
|
|
30
|
+
|
|
31
|
+
function makeTempDir(): string {
|
|
32
|
+
const dir = realpathSync(mkdtempSync(join(tmpdir(), "file-list-test-")));
|
|
33
|
+
testDirs.push(dir);
|
|
34
|
+
return dir;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
afterEach(() => {
|
|
38
|
+
for (const dir of testDirs.splice(0)) {
|
|
39
|
+
rmSync(dir, { recursive: true, force: true });
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
/** Build a sandbox-bound PathPolicy for the given directory. */
|
|
44
|
+
function sandboxPolicyFor(boundary: string): PathPolicy {
|
|
45
|
+
return (rawPath, options) => sandboxPolicy(rawPath, boundary, options);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
// listDirSafe
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
describe("FileSystemOps.listDirSafe", () => {
|
|
53
|
+
test("lists directory contents with type indicators (dirs end with /)", () => {
|
|
54
|
+
const dir = makeTempDir();
|
|
55
|
+
mkdirSync(join(dir, "subdir-a"));
|
|
56
|
+
mkdirSync(join(dir, "subdir-b"));
|
|
57
|
+
writeFileSync(join(dir, "file-a.txt"), "hello");
|
|
58
|
+
writeFileSync(join(dir, "file-b.md"), "world");
|
|
59
|
+
|
|
60
|
+
const ops = new FileSystemOps(sandboxPolicyFor(dir));
|
|
61
|
+
const result = ops.listDirSafe({ path: dir });
|
|
62
|
+
expect(result.ok).toBe(true);
|
|
63
|
+
if (!result.ok) return;
|
|
64
|
+
|
|
65
|
+
const lines = result.value.listing.split("\n");
|
|
66
|
+
// Directories first with trailing /
|
|
67
|
+
expect(lines[0]).toBe("subdir-a/");
|
|
68
|
+
expect(lines[1]).toBe("subdir-b/");
|
|
69
|
+
// Files after directories, with size info
|
|
70
|
+
expect(lines[2]).toMatch(/^file-a\.txt\s+\d+\s*B$/);
|
|
71
|
+
expect(lines[3]).toMatch(/^file-b\.md\s+\d+\s*B$/);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("glob filtering works (e.g. '*.md' only returns .md files)", () => {
|
|
75
|
+
const dir = makeTempDir();
|
|
76
|
+
writeFileSync(join(dir, "readme.md"), "# Title");
|
|
77
|
+
writeFileSync(join(dir, "notes.md"), "some notes");
|
|
78
|
+
writeFileSync(join(dir, "app.ts"), "console.log('hi')");
|
|
79
|
+
writeFileSync(join(dir, "config.json"), "{}");
|
|
80
|
+
|
|
81
|
+
const ops = new FileSystemOps(sandboxPolicyFor(dir));
|
|
82
|
+
const result = ops.listDirSafe({ path: dir, glob: "*.md" });
|
|
83
|
+
expect(result.ok).toBe(true);
|
|
84
|
+
if (!result.ok) return;
|
|
85
|
+
|
|
86
|
+
const lines = result.value.listing.split("\n");
|
|
87
|
+
expect(lines.length).toBe(2);
|
|
88
|
+
expect(lines[0]).toMatch(/^notes\.md/);
|
|
89
|
+
expect(lines[1]).toMatch(/^readme\.md/);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("returns NOT_A_DIRECTORY error for file paths", () => {
|
|
93
|
+
const dir = makeTempDir();
|
|
94
|
+
writeFileSync(join(dir, "regular-file.txt"), "content");
|
|
95
|
+
|
|
96
|
+
const ops = new FileSystemOps(sandboxPolicyFor(dir));
|
|
97
|
+
const result = ops.listDirSafe({ path: join(dir, "regular-file.txt") });
|
|
98
|
+
expect(result.ok).toBe(false);
|
|
99
|
+
if (result.ok) return;
|
|
100
|
+
expect(result.error.code).toBe("NOT_A_DIRECTORY");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("returns NOT_FOUND error for nonexistent paths", () => {
|
|
104
|
+
const dir = makeTempDir();
|
|
105
|
+
|
|
106
|
+
const ops = new FileSystemOps(sandboxPolicyFor(dir));
|
|
107
|
+
const result = ops.listDirSafe({ path: join(dir, "nonexistent") });
|
|
108
|
+
expect(result.ok).toBe(false);
|
|
109
|
+
if (result.ok) return;
|
|
110
|
+
expect(result.error.code).toBe("NOT_FOUND");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("sandbox policy rejects paths outside boundary", () => {
|
|
114
|
+
const dir = makeTempDir();
|
|
115
|
+
|
|
116
|
+
const ops = new FileSystemOps(sandboxPolicyFor(dir));
|
|
117
|
+
const result = ops.listDirSafe({ path: "../../../etc" });
|
|
118
|
+
expect(result.ok).toBe(false);
|
|
119
|
+
if (result.ok) return;
|
|
120
|
+
expect(result.error.code).toBe("PATH_OUT_OF_BOUNDS");
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
// FileListTool integration tests
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
|
|
128
|
+
/** Build a minimal ToolContext with the given workingDir. */
|
|
129
|
+
function makeToolContext(workingDir: string): ToolContext {
|
|
130
|
+
return {
|
|
131
|
+
workingDir,
|
|
132
|
+
conversationId: "test-conv",
|
|
133
|
+
trustClass: "guardian",
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
describe("FileListTool", () => {
|
|
138
|
+
test("execute() returns formatted listing for a temp directory", async () => {
|
|
139
|
+
const dir = makeTempDir();
|
|
140
|
+
mkdirSync(join(dir, "src"));
|
|
141
|
+
writeFileSync(join(dir, "README.md"), "# Hello");
|
|
142
|
+
writeFileSync(join(dir, "index.ts"), "export {}");
|
|
143
|
+
|
|
144
|
+
const result = await fileListTool.execute(
|
|
145
|
+
{ path: dir, activity: "listing test dir" },
|
|
146
|
+
makeToolContext(dir),
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
expect(result.isError).toBe(false);
|
|
150
|
+
const lines = result.content.split("\n");
|
|
151
|
+
// Directory first with trailing /
|
|
152
|
+
expect(lines[0]).toBe("src/");
|
|
153
|
+
// Files after (alphabetical), with size info
|
|
154
|
+
expect(lines[1]).toMatch(/^index\.ts\s+\d+\s*B$/);
|
|
155
|
+
expect(lines[2]).toMatch(/^README\.md\s+\d+\s*B$/);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("execute() returns error for invalid path input", async () => {
|
|
159
|
+
const dir = makeTempDir();
|
|
160
|
+
const result = await fileListTool.execute(
|
|
161
|
+
{ path: 123, activity: "test" },
|
|
162
|
+
makeToolContext(dir),
|
|
163
|
+
);
|
|
164
|
+
expect(result.isError).toBe(true);
|
|
165
|
+
expect(result.content).toContain("path is required and must be a string");
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("execute() returns error for nonexistent directory", async () => {
|
|
169
|
+
const dir = makeTempDir();
|
|
170
|
+
const result = await fileListTool.execute(
|
|
171
|
+
{ path: join(dir, "nope"), activity: "test" },
|
|
172
|
+
makeToolContext(dir),
|
|
173
|
+
);
|
|
174
|
+
expect(result.isError).toBe(true);
|
|
175
|
+
expect(result.content).toContain("not found");
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
// Subagent-only visibility
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
|
|
183
|
+
describe("file_list subagent visibility", () => {
|
|
184
|
+
test("file_list is in SUBAGENT_ONLY_TOOL_NAMES", () => {
|
|
185
|
+
expect(SUBAGENT_ONLY_TOOL_NAMES.has("file_list")).toBe(true);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("isToolActiveForContext hides file_list when isSubagent is false", () => {
|
|
189
|
+
const ctx: SkillProjectionContext = {
|
|
190
|
+
skillProjectionState: new Map(),
|
|
191
|
+
skillProjectionCache: {},
|
|
192
|
+
coreToolNames: new Set(["file_list"]),
|
|
193
|
+
toolsDisabledDepth: 0,
|
|
194
|
+
};
|
|
195
|
+
expect(isToolActiveForContext("file_list", ctx)).toBe(false);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test("isToolActiveForContext hides file_list when isSubagent is undefined", () => {
|
|
199
|
+
const ctx: SkillProjectionContext = {
|
|
200
|
+
skillProjectionState: new Map(),
|
|
201
|
+
skillProjectionCache: {},
|
|
202
|
+
coreToolNames: new Set(["file_list"]),
|
|
203
|
+
toolsDisabledDepth: 0,
|
|
204
|
+
isSubagent: undefined,
|
|
205
|
+
};
|
|
206
|
+
expect(isToolActiveForContext("file_list", ctx)).toBe(false);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test("isToolActiveForContext shows file_list when isSubagent is true", () => {
|
|
210
|
+
const ctx: SkillProjectionContext = {
|
|
211
|
+
skillProjectionState: new Map(),
|
|
212
|
+
skillProjectionCache: {},
|
|
213
|
+
coreToolNames: new Set(["file_list"]),
|
|
214
|
+
toolsDisabledDepth: 0,
|
|
215
|
+
isSubagent: true,
|
|
216
|
+
};
|
|
217
|
+
expect(isToolActiveForContext("file_list", ctx)).toBe(true);
|
|
218
|
+
});
|
|
219
|
+
});
|
|
@@ -53,7 +53,7 @@ describe("first-greeting", () => {
|
|
|
53
53
|
const greeting = getCannedFirstGreeting();
|
|
54
54
|
expect(greeting).toBe(CANNED_FIRST_GREETING);
|
|
55
55
|
expect(greeting).toContain("brand new");
|
|
56
|
-
expect(greeting).toContain("
|
|
56
|
+
expect(greeting).toContain("No name, no memories");
|
|
57
57
|
});
|
|
58
58
|
});
|
|
59
59
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { rmSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
4
4
|
|
|
@@ -18,6 +18,9 @@ let mockConfig = {
|
|
|
18
18
|
mock.module("../config/loader.js", () => ({
|
|
19
19
|
getConfig: () => mockConfig,
|
|
20
20
|
loadConfig: () => mockConfig,
|
|
21
|
+
loadRawConfig: () => ({}),
|
|
22
|
+
saveRawConfig: () => {},
|
|
23
|
+
invalidateConfigCache: () => {},
|
|
21
24
|
}));
|
|
22
25
|
|
|
23
26
|
// Mock conversation store
|
|
@@ -43,6 +46,7 @@ mock.module("../memory/conversation-crud.js", () => ({
|
|
|
43
46
|
totalEstimatedCost: 0,
|
|
44
47
|
title: null,
|
|
45
48
|
}),
|
|
49
|
+
getMessageById: () => null,
|
|
46
50
|
provenanceFromTrustContext: () => ({
|
|
47
51
|
source: "user",
|
|
48
52
|
trustContext: undefined,
|
|
@@ -72,7 +76,16 @@ mock.module("../memory/conversation-title-service.js", () => ({
|
|
|
72
76
|
}));
|
|
73
77
|
|
|
74
78
|
// Import after mocks are set up
|
|
75
|
-
const { HeartbeatService } =
|
|
79
|
+
const { HeartbeatService, isShallowProfile } =
|
|
80
|
+
await import("../heartbeat/heartbeat-service.js");
|
|
81
|
+
|
|
82
|
+
// Read the bundled template files so we can write them into the test workspace
|
|
83
|
+
const templatesDir = join(import.meta.dirname!, "..", "prompts", "templates");
|
|
84
|
+
const IDENTITY_TEMPLATE = readFileSync(
|
|
85
|
+
join(templatesDir, "IDENTITY.md"),
|
|
86
|
+
"utf-8",
|
|
87
|
+
);
|
|
88
|
+
const USER_TEMPLATE = readFileSync(join(templatesDir, "USER.md"), "utf-8");
|
|
76
89
|
|
|
77
90
|
describe("HeartbeatService", () => {
|
|
78
91
|
let processMessageCalls: Array<{
|
|
@@ -83,8 +96,11 @@ describe("HeartbeatService", () => {
|
|
|
83
96
|
let alerterCalls: Array<{ type: string; title: string; body: string }>;
|
|
84
97
|
|
|
85
98
|
afterEach(() => {
|
|
86
|
-
// Clean up
|
|
99
|
+
// Clean up workspace files between tests so file-existence tests don't leak
|
|
87
100
|
rmSync(join(testWorkspaceDir, "HEARTBEAT.md"), { force: true });
|
|
101
|
+
rmSync(join(testWorkspaceDir, "IDENTITY.md"), { force: true });
|
|
102
|
+
rmSync(join(testWorkspaceDir, "USER.md"), { force: true });
|
|
103
|
+
rmSync(join(testWorkspaceDir, ".reengagement-ts"), { force: true });
|
|
88
104
|
});
|
|
89
105
|
|
|
90
106
|
beforeEach(() => {
|
|
@@ -228,6 +244,24 @@ describe("HeartbeatService", () => {
|
|
|
228
244
|
expect(processMessageCalls).toHaveLength(0);
|
|
229
245
|
});
|
|
230
246
|
|
|
247
|
+
test("active hours skip still advances nextRunAt", async () => {
|
|
248
|
+
mockConfig.heartbeat.activeHoursStart = 9;
|
|
249
|
+
mockConfig.heartbeat.activeHoursEnd = 17;
|
|
250
|
+
|
|
251
|
+
const service = createService({ getCurrentHour: () => 3 });
|
|
252
|
+
service.start();
|
|
253
|
+
|
|
254
|
+
const before = Date.now();
|
|
255
|
+
await service.runOnce();
|
|
256
|
+
|
|
257
|
+
expect(processMessageCalls).toHaveLength(0);
|
|
258
|
+
expect(service.nextRunAt).not.toBeNull();
|
|
259
|
+
expect(service.nextRunAt!).toBeGreaterThanOrEqual(
|
|
260
|
+
before + mockConfig.heartbeat.intervalMs,
|
|
261
|
+
);
|
|
262
|
+
service.stop();
|
|
263
|
+
});
|
|
264
|
+
|
|
231
265
|
test("active hours guard allows within window", async () => {
|
|
232
266
|
mockConfig.heartbeat.activeHoursStart = 9;
|
|
233
267
|
mockConfig.heartbeat.activeHoursEnd = 17;
|
|
@@ -360,6 +394,22 @@ describe("HeartbeatService", () => {
|
|
|
360
394
|
expect(alerterCalls[0].body).toBe("LLM timeout");
|
|
361
395
|
});
|
|
362
396
|
|
|
397
|
+
test("successful run updates lastRunAt and nextRunAt", async () => {
|
|
398
|
+
const service = createService();
|
|
399
|
+
expect(service.lastRunAt).toBeNull();
|
|
400
|
+
expect(service.nextRunAt).toBeNull();
|
|
401
|
+
|
|
402
|
+
const before = Date.now();
|
|
403
|
+
await service.runOnce();
|
|
404
|
+
|
|
405
|
+
expect(service.lastRunAt).not.toBeNull();
|
|
406
|
+
expect(service.lastRunAt!).toBeGreaterThanOrEqual(before);
|
|
407
|
+
expect(service.nextRunAt).not.toBeNull();
|
|
408
|
+
expect(service.nextRunAt!).toBeGreaterThanOrEqual(
|
|
409
|
+
before + mockConfig.heartbeat.intervalMs,
|
|
410
|
+
);
|
|
411
|
+
});
|
|
412
|
+
|
|
363
413
|
test("alerts on conversation creation failure", async () => {
|
|
364
414
|
// Override createConversation to throw via a fresh import trick:
|
|
365
415
|
// Since createConversation is mocked at module level, we simulate
|
|
@@ -442,4 +492,131 @@ describe("HeartbeatService", () => {
|
|
|
442
492
|
expect(processMessageCalls).toHaveLength(1);
|
|
443
493
|
expect(processMessageCalls[0].options?.speed).toBe("fast");
|
|
444
494
|
});
|
|
495
|
+
|
|
496
|
+
describe("isShallowProfile", () => {
|
|
497
|
+
test("returns true when both IDENTITY.md and USER.md are unmodified templates", () => {
|
|
498
|
+
writeFileSync(join(testWorkspaceDir, "IDENTITY.md"), IDENTITY_TEMPLATE);
|
|
499
|
+
writeFileSync(join(testWorkspaceDir, "USER.md"), USER_TEMPLATE);
|
|
500
|
+
|
|
501
|
+
expect(isShallowProfile()).toBe(true);
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
test("returns false when IDENTITY.md has been customized", () => {
|
|
505
|
+
writeFileSync(
|
|
506
|
+
join(testWorkspaceDir, "IDENTITY.md"),
|
|
507
|
+
"# IDENTITY.md\n\n- **Name:** Jarvis\n",
|
|
508
|
+
);
|
|
509
|
+
writeFileSync(join(testWorkspaceDir, "USER.md"), USER_TEMPLATE);
|
|
510
|
+
|
|
511
|
+
expect(isShallowProfile()).toBe(false);
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
test("returns false when USER.md has been customized", () => {
|
|
515
|
+
writeFileSync(join(testWorkspaceDir, "IDENTITY.md"), IDENTITY_TEMPLATE);
|
|
516
|
+
writeFileSync(
|
|
517
|
+
join(testWorkspaceDir, "USER.md"),
|
|
518
|
+
"# USER.md\n\n- Preferred name/reference: Alice\n",
|
|
519
|
+
);
|
|
520
|
+
|
|
521
|
+
expect(isShallowProfile()).toBe(false);
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
test("returns false when neither file exists", () => {
|
|
525
|
+
expect(isShallowProfile()).toBe(false);
|
|
526
|
+
});
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
describe("relationship-depth prompt injection", () => {
|
|
530
|
+
test("includes <relationship-depth> when profile is shallow", () => {
|
|
531
|
+
writeFileSync(join(testWorkspaceDir, "IDENTITY.md"), IDENTITY_TEMPLATE);
|
|
532
|
+
writeFileSync(join(testWorkspaceDir, "USER.md"), USER_TEMPLATE);
|
|
533
|
+
|
|
534
|
+
const service = createService();
|
|
535
|
+
const { prompt, includedReengagement } =
|
|
536
|
+
service.buildPrompt("- Check things");
|
|
537
|
+
|
|
538
|
+
expect(prompt).toContain("<relationship-depth>");
|
|
539
|
+
expect(prompt).toContain("profile is still sparse");
|
|
540
|
+
expect(includedReengagement).toBe(true);
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
test("omits <relationship-depth> when profile is not shallow", () => {
|
|
544
|
+
writeFileSync(
|
|
545
|
+
join(testWorkspaceDir, "IDENTITY.md"),
|
|
546
|
+
"# IDENTITY.md\n\n- **Name:** Jarvis\n",
|
|
547
|
+
);
|
|
548
|
+
writeFileSync(join(testWorkspaceDir, "USER.md"), USER_TEMPLATE);
|
|
549
|
+
|
|
550
|
+
const service = createService();
|
|
551
|
+
const { prompt, includedReengagement } =
|
|
552
|
+
service.buildPrompt("- Check things");
|
|
553
|
+
|
|
554
|
+
expect(prompt).not.toContain("<relationship-depth>");
|
|
555
|
+
expect(includedReengagement).toBe(false);
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
test("omits <relationship-depth> when cooldown has not elapsed", () => {
|
|
559
|
+
writeFileSync(join(testWorkspaceDir, "IDENTITY.md"), IDENTITY_TEMPLATE);
|
|
560
|
+
writeFileSync(join(testWorkspaceDir, "USER.md"), USER_TEMPLATE);
|
|
561
|
+
// Write a recent timestamp to simulate cooldown not elapsed
|
|
562
|
+
writeFileSync(
|
|
563
|
+
join(testWorkspaceDir, ".reengagement-ts"),
|
|
564
|
+
Date.now().toString(),
|
|
565
|
+
);
|
|
566
|
+
|
|
567
|
+
const service = createService();
|
|
568
|
+
const { prompt, includedReengagement } =
|
|
569
|
+
service.buildPrompt("- Check things");
|
|
570
|
+
|
|
571
|
+
expect(prompt).not.toContain("<relationship-depth>");
|
|
572
|
+
expect(includedReengagement).toBe(false);
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
test("includes <relationship-depth> when cooldown has elapsed", () => {
|
|
576
|
+
writeFileSync(join(testWorkspaceDir, "IDENTITY.md"), IDENTITY_TEMPLATE);
|
|
577
|
+
writeFileSync(join(testWorkspaceDir, "USER.md"), USER_TEMPLATE);
|
|
578
|
+
// Write a timestamp from 19 hours ago
|
|
579
|
+
const nineteenHoursAgo = Date.now() - 19 * 60 * 60 * 1000;
|
|
580
|
+
writeFileSync(
|
|
581
|
+
join(testWorkspaceDir, ".reengagement-ts"),
|
|
582
|
+
nineteenHoursAgo.toString(),
|
|
583
|
+
);
|
|
584
|
+
|
|
585
|
+
const service = createService();
|
|
586
|
+
const { prompt, includedReengagement } =
|
|
587
|
+
service.buildPrompt("- Check things");
|
|
588
|
+
|
|
589
|
+
expect(prompt).toContain("<relationship-depth>");
|
|
590
|
+
expect(includedReengagement).toBe(true);
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
test("does not record timestamp when processMessage fails", async () => {
|
|
594
|
+
writeFileSync(join(testWorkspaceDir, "IDENTITY.md"), IDENTITY_TEMPLATE);
|
|
595
|
+
writeFileSync(join(testWorkspaceDir, "USER.md"), USER_TEMPLATE);
|
|
596
|
+
|
|
597
|
+
const service = createService({
|
|
598
|
+
processMessage: async () => {
|
|
599
|
+
throw new Error("LLM timeout");
|
|
600
|
+
},
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
await service.runOnce();
|
|
604
|
+
|
|
605
|
+
// The reengagement timestamp file should NOT exist since delivery failed
|
|
606
|
+
const tsPath = join(testWorkspaceDir, ".reengagement-ts");
|
|
607
|
+
expect(existsSync(tsPath)).toBe(false);
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
test("records timestamp after successful delivery", async () => {
|
|
611
|
+
writeFileSync(join(testWorkspaceDir, "IDENTITY.md"), IDENTITY_TEMPLATE);
|
|
612
|
+
writeFileSync(join(testWorkspaceDir, "USER.md"), USER_TEMPLATE);
|
|
613
|
+
|
|
614
|
+
const service = createService();
|
|
615
|
+
await service.runOnce();
|
|
616
|
+
|
|
617
|
+
// The reengagement timestamp file should exist after successful delivery
|
|
618
|
+
const tsPath = join(testWorkspaceDir, ".reengagement-ts");
|
|
619
|
+
expect(existsSync(tsPath)).toBe(true);
|
|
620
|
+
});
|
|
621
|
+
});
|
|
445
622
|
});
|