agent-mockingbird 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/btca-cli/SKILL.md +64 -0
- package/.agents/skills/btca-cli/agents/openai.yaml +3 -0
- package/.agents/skills/frontend-design/SKILL.md +42 -0
- package/.agents/skills/frontend-design/agents/openai.yaml +3 -0
- package/.env.example +36 -0
- package/.githooks/pre-commit +33 -0
- package/.github/workflows/ci.yml +309 -0
- package/.opencode/bun.lock +18 -0
- package/.opencode/package.json +5 -0
- package/.opencode/tools/agent_type_manager.ts +100 -0
- package/.opencode/tools/config_manager.ts +87 -0
- package/.opencode/tools/cron_manager.ts +145 -0
- package/.opencode/tools/memory_get.ts +43 -0
- package/.opencode/tools/memory_remember.ts +53 -0
- package/.opencode/tools/memory_search.ts +48 -0
- package/AGENTS.md +126 -0
- package/MEMORY.md +2 -0
- package/README.md +451 -0
- package/THIRD_PARTY_NOTICES.md +11 -0
- package/agent-mockingbird.config.example.json +135 -0
- package/apps/server/package.json +32 -0
- package/apps/server/src/backend/agents/bootstrapContext.ts +362 -0
- package/apps/server/src/backend/agents/openclawImport.test.ts +133 -0
- package/apps/server/src/backend/agents/openclawImport.ts +797 -0
- package/apps/server/src/backend/agents/opencodeConfig.ts +428 -0
- package/apps/server/src/backend/agents/service.ts +10 -0
- package/apps/server/src/backend/config/example-config.test.ts +20 -0
- package/apps/server/src/backend/config/orchestration.ts +243 -0
- package/apps/server/src/backend/config/policy.ts +158 -0
- package/apps/server/src/backend/config/schema.test.ts +15 -0
- package/apps/server/src/backend/config/schema.ts +391 -0
- package/apps/server/src/backend/config/semantic.test.ts +34 -0
- package/apps/server/src/backend/config/semantic.ts +149 -0
- package/apps/server/src/backend/config/service.test.ts +75 -0
- package/apps/server/src/backend/config/service.ts +207 -0
- package/apps/server/src/backend/config/smoke.ts +77 -0
- package/apps/server/src/backend/config/store.test.ts +123 -0
- package/apps/server/src/backend/config/store.ts +581 -0
- package/apps/server/src/backend/config/testFixtures.ts +5 -0
- package/apps/server/src/backend/config/types.ts +56 -0
- package/apps/server/src/backend/contracts/events.ts +320 -0
- package/apps/server/src/backend/contracts/runtime.ts +111 -0
- package/apps/server/src/backend/cron/executor.ts +435 -0
- package/apps/server/src/backend/cron/repository.ts +170 -0
- package/apps/server/src/backend/cron/service.ts +660 -0
- package/apps/server/src/backend/cron/storage.ts +92 -0
- package/apps/server/src/backend/cron/types.ts +138 -0
- package/apps/server/src/backend/cron/utils.ts +351 -0
- package/apps/server/src/backend/db/client.ts +20 -0
- package/apps/server/src/backend/db/migrate.ts +40 -0
- package/apps/server/src/backend/db/repository.ts +1762 -0
- package/apps/server/src/backend/db/schema.ts +113 -0
- package/apps/server/src/backend/db/usageDashboard.test.ts +102 -0
- package/apps/server/src/backend/db/wipe.ts +13 -0
- package/apps/server/src/backend/defaults.ts +32 -0
- package/apps/server/src/backend/env.ts +48 -0
- package/apps/server/src/backend/heartbeat/activeHours.ts +45 -0
- package/apps/server/src/backend/heartbeat/defaultJob.ts +88 -0
- package/apps/server/src/backend/heartbeat/heartbeat.test.ts +110 -0
- package/apps/server/src/backend/heartbeat/runtimeService.ts +190 -0
- package/apps/server/src/backend/heartbeat/service.ts +176 -0
- package/apps/server/src/backend/heartbeat/state.test.ts +63 -0
- package/apps/server/src/backend/heartbeat/state.ts +167 -0
- package/apps/server/src/backend/heartbeat/types.ts +54 -0
- package/apps/server/src/backend/http/boundedQueue.test.ts +49 -0
- package/apps/server/src/backend/http/boundedQueue.ts +92 -0
- package/apps/server/src/backend/http/parsers.ts +40 -0
- package/apps/server/src/backend/http/router.ts +61 -0
- package/apps/server/src/backend/http/routes/agentRoutes.ts +67 -0
- package/apps/server/src/backend/http/routes/backgroundRoutes.ts +203 -0
- package/apps/server/src/backend/http/routes/chatRoutes.ts +107 -0
- package/apps/server/src/backend/http/routes/configRoutes.ts +602 -0
- package/apps/server/src/backend/http/routes/cronRoutes.ts +221 -0
- package/apps/server/src/backend/http/routes/dashboardRoutes.ts +308 -0
- package/apps/server/src/backend/http/routes/eventRoutes.ts +7 -0
- package/apps/server/src/backend/http/routes/heartbeatRoutes.test.ts +41 -0
- package/apps/server/src/backend/http/routes/heartbeatRoutes.ts +28 -0
- package/apps/server/src/backend/http/routes/index.ts +101 -0
- package/apps/server/src/backend/http/routes/mcpRoutes.ts +213 -0
- package/apps/server/src/backend/http/routes/memoryRoutes.ts +154 -0
- package/apps/server/src/backend/http/routes/runRoutes.ts +310 -0
- package/apps/server/src/backend/http/routes/runtimeRoutes.ts +197 -0
- package/apps/server/src/backend/http/routes/skillRoutes.ts +112 -0
- package/apps/server/src/backend/http/routes/uiRoutes.test.ts +161 -0
- package/apps/server/src/backend/http/routes/uiRoutes.ts +177 -0
- package/apps/server/src/backend/http/routes/usageRoutes.test.ts +104 -0
- package/apps/server/src/backend/http/routes/usageRoutes.ts +767 -0
- package/apps/server/src/backend/http/schemas.ts +64 -0
- package/apps/server/src/backend/http/sse.ts +144 -0
- package/apps/server/src/backend/integration/backend-core.test.ts +2316 -0
- package/apps/server/src/backend/logging/logger.ts +64 -0
- package/apps/server/src/backend/mcp/service.ts +326 -0
- package/apps/server/src/backend/memory/cli.ts +170 -0
- package/apps/server/src/backend/memory/conceptExpansion.test.ts +28 -0
- package/apps/server/src/backend/memory/conceptExpansion.ts +80 -0
- package/apps/server/src/backend/memory/qmdPort.test.ts +54 -0
- package/apps/server/src/backend/memory/qmdPort.ts +61 -0
- package/apps/server/src/backend/memory/records.test.ts +66 -0
- package/apps/server/src/backend/memory/records.ts +229 -0
- package/apps/server/src/backend/memory/service.ts +2012 -0
- package/apps/server/src/backend/memory/sqliteVec.ts +58 -0
- package/apps/server/src/backend/memory/types.ts +104 -0
- package/apps/server/src/backend/opencode/agentMockingbirdPlugin.test.ts +396 -0
- package/apps/server/src/backend/opencode/client.ts +98 -0
- package/apps/server/src/backend/opencode/models.ts +41 -0
- package/apps/server/src/backend/opencode/systemPrompt.test.ts +146 -0
- package/apps/server/src/backend/opencode/systemPrompt.ts +284 -0
- package/apps/server/src/backend/paths.ts +57 -0
- package/apps/server/src/backend/prompts/service.ts +100 -0
- package/apps/server/src/backend/queue/queue.test.ts +189 -0
- package/apps/server/src/backend/queue/service.ts +177 -0
- package/apps/server/src/backend/queue/types.ts +39 -0
- package/apps/server/src/backend/run/service.ts +576 -0
- package/apps/server/src/backend/run/storage.ts +47 -0
- package/apps/server/src/backend/run/types.ts +44 -0
- package/apps/server/src/backend/runtime/errors.ts +61 -0
- package/apps/server/src/backend/runtime/index.ts +72 -0
- package/apps/server/src/backend/runtime/memoryPromptDedup.test.ts +153 -0
- package/apps/server/src/backend/runtime/memoryPromptDedup.ts +76 -0
- package/apps/server/src/backend/runtime/opencodeRuntime/backgroundMethods.ts +765 -0
- package/apps/server/src/backend/runtime/opencodeRuntime/coreMethods.ts +705 -0
- package/apps/server/src/backend/runtime/opencodeRuntime/eventMethods.ts +503 -0
- package/apps/server/src/backend/runtime/opencodeRuntime/memoryMethods.ts +462 -0
- package/apps/server/src/backend/runtime/opencodeRuntime/promptMethods.ts +1167 -0
- package/apps/server/src/backend/runtime/opencodeRuntime/shared.ts +254 -0
- package/apps/server/src/backend/runtime/opencodeRuntime.test.ts +2899 -0
- package/apps/server/src/backend/runtime/opencodeRuntime.ts +135 -0
- package/apps/server/src/backend/runtime/sessionScope.ts +45 -0
- package/apps/server/src/backend/skills/service.ts +442 -0
- package/apps/server/src/backend/workspace/resolve.ts +27 -0
- package/apps/server/src/cli/agent-mockingbird.mjs +2522 -0
- package/apps/server/src/cli/agent-mockingbird.test.ts +68 -0
- package/apps/server/src/cli/runtime-assets.mjs +269 -0
- package/apps/server/src/cli/runtime-assets.test.ts +52 -0
- package/apps/server/src/cli/runtime-layout.mjs +75 -0
- package/apps/server/src/cli/standaloneBuild.test.ts +19 -0
- package/apps/server/src/cli/standaloneBuild.ts +19 -0
- package/apps/server/src/cli/standaloneCronBinary.test.ts +187 -0
- package/apps/server/src/index.ts +178 -0
- package/apps/server/tsconfig.json +12 -0
- package/backlog.md +5 -0
- package/bin/agent-mockingbird +2522 -0
- package/bin/runtime-layout.mjs +75 -0
- package/build-bin.ts +34 -0
- package/build-cli.mjs +37 -0
- package/build.ts +40 -0
- package/bun-env.d.ts +11 -0
- package/bun.lock +888 -0
- package/bunfig.toml +2 -0
- package/components.json +21 -0
- package/config.json +130 -0
- package/deploy/RELEASE_INSTALL.md +112 -0
- package/deploy/docker-compose.yml +42 -0
- package/deploy/systemd/README.md +46 -0
- package/deploy/systemd/agent-mockingbird.service +28 -0
- package/deploy/systemd/opencode.service +25 -0
- package/docs/legacy-config-ui-reference.md +51 -0
- package/docs/memory-e2e-trace-2026-03-04.md +63 -0
- package/docs/memory-ops.md +96 -0
- package/docs/memory-runtime-contract.md +42 -0
- package/docs/memory-tuning-remote-2026-03-04.md +59 -0
- package/docs/opencode-rebase-workflow-plan.md +614 -0
- package/docs/opencode-startup-sync-plan.md +94 -0
- package/docs/vendor-opencode.md +41 -0
- package/drizzle/0000_famous_turbo.sql +49 -0
- package/drizzle/0001_cron_memory_aux.sql +160 -0
- package/drizzle/0002_runtime_session_bindings.sql +28 -0
- package/drizzle/0003_background_runs.sql +27 -0
- package/drizzle/0004_memory_open_write.sql +63 -0
- package/drizzle/0005_signal_channel.sql +47 -0
- package/drizzle/0006_usage_event_dimensions.sql +7 -0
- package/drizzle/meta/0000_snapshot.json +341 -0
- package/drizzle/meta/_journal.json +55 -0
- package/drizzle.config.ts +14 -0
- package/eslint.config.mjs +77 -0
- package/knip.json +18 -0
- package/memory/2026-03-04.md +4 -0
- package/opencode.lock.json +16 -0
- package/package.json +67 -0
- package/packages/agent-mockingbird-installer/README.md +31 -0
- package/packages/agent-mockingbird-installer/bin/agent-mockingbird-installer.mjs +44 -0
- package/packages/agent-mockingbird-installer/opencode.lock.json +16 -0
- package/packages/agent-mockingbird-installer/package.json +23 -0
- package/packages/contracts/package.json +19 -0
- package/packages/contracts/src/agentTypes.ts +122 -0
- package/packages/contracts/src/cron.ts +146 -0
- package/packages/contracts/src/dashboard.ts +378 -0
- package/packages/contracts/src/index.ts +3 -0
- package/packages/contracts/tsconfig.json +4 -0
- package/patches/opencode/0001-Wafflebot-OpenCode-baseline.patch +2341 -0
- package/patches/opencode/0002-Fix-OpenCode-web-entry-and-settings-icons.patch +104 -0
- package/patches/opencode/0003-fix-app-remove-duplicate-sidebar-mount.patch +32 -0
- package/patches/opencode/0004-Add-heartbeat-settings-and-usage-nav.patch +506 -0
- package/patches/opencode/0005-Use-chart-icon-for-usage-nav.patch +38 -0
- package/patches/opencode/0006-Modernize-cron-settings.patch +399 -0
- package/patches/opencode/0007-Rename-waffle-namespaces-to-mockingbird.patch +1110 -0
- package/patches/opencode/0008-Remove-cron-contract-section.patch +178 -0
- package/patches/opencode/0009-Rework-cron-tab-as-operations-console.patch +414 -0
- package/patches/opencode/0010-Refine-heartbeat-settings-controls.patch +208 -0
- package/runtime-assets/opencode-config/opencode.jsonc +25 -0
- package/runtime-assets/opencode-config/package.json +5 -0
- package/runtime-assets/opencode-config/plugins/agent-mockingbird.ts +715 -0
- package/runtime-assets/workspace/.agents/skills/config-auditor/SKILL.md +25 -0
- package/runtime-assets/workspace/.agents/skills/config-editor/SKILL.md +24 -0
- package/runtime-assets/workspace/.agents/skills/cron-manager/SKILL.md +57 -0
- package/runtime-assets/workspace/.agents/skills/memory-ops/SKILL.md +120 -0
- package/runtime-assets/workspace/.agents/skills/runtime-diagnose/SKILL.md +25 -0
- package/runtime-assets/workspace/AGENTS.md +56 -0
- package/runtime-assets/workspace/MEMORY.md +4 -0
- package/scripts/build-release-bundle.sh +66 -0
- package/scripts/check-ship.ts +383 -0
- package/scripts/dev-opencode.sh +17 -0
- package/scripts/dev-stack-opencode.sh +15 -0
- package/scripts/dev-stack.sh +61 -0
- package/scripts/install-systemd.sh +87 -0
- package/scripts/memory-e2e.sh +76 -0
- package/scripts/memory-trace-e2e.sh +141 -0
- package/scripts/migrate-opencode-env.ts +108 -0
- package/scripts/onboard/bootstrap.sh +32 -0
- package/scripts/opencode-swap.ts +78 -0
- package/scripts/opencode-sync.ts +715 -0
- package/scripts/runtime-assets-sync.mjs +83 -0
- package/scripts/setup-git-hooks.ts +39 -0
- package/tsconfig.json +45 -0
- package/tui.json +98 -0
- package/turbo.json +36 -0
- package/vendor/OPENCODE_VENDOR.md +13 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import type { ConfigProvidersResponse } from "@opencode-ai/sdk/client";
|
|
2
|
+
|
|
3
|
+
import type { AgentMockingbirdConfig } from "./schema";
|
|
4
|
+
import { ConfigApplyError, type ConfigSemanticSummary } from "./types";
|
|
5
|
+
import { createOpencodeClientFromConnection, unwrapSdkData } from "../opencode/client";
|
|
6
|
+
|
|
7
|
+
export function resolveModelRefForValidation(
|
|
8
|
+
rawRef: string,
|
|
9
|
+
defaultProviderId: string,
|
|
10
|
+
modelsByProvider: Map<string, Set<string>>,
|
|
11
|
+
) {
|
|
12
|
+
const trimmed = rawRef.trim();
|
|
13
|
+
const defaultProvider = defaultProviderId.trim();
|
|
14
|
+
if (!trimmed) {
|
|
15
|
+
throw new ConfigApplyError("schema", "Model reference must not be empty");
|
|
16
|
+
}
|
|
17
|
+
if (!defaultProvider) {
|
|
18
|
+
throw new ConfigApplyError("schema", "Default provider must not be empty");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Model IDs can contain "/" (for example "zai-org/GLM-4.7-Flash"). Prefer
|
|
22
|
+
// exact match on the selected provider before treating the value as a
|
|
23
|
+
// qualified "provider/model" reference.
|
|
24
|
+
const defaultProviderModels = modelsByProvider.get(defaultProvider);
|
|
25
|
+
if (defaultProviderModels?.has(trimmed)) {
|
|
26
|
+
return { providerId: defaultProvider, modelId: trimmed };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!trimmed.includes("/")) {
|
|
30
|
+
return { providerId: defaultProvider, modelId: trimmed };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const [providerCandidate = "", ...rest] = trimmed.split("/");
|
|
34
|
+
const modelId = rest.join("/").trim();
|
|
35
|
+
const providerId = providerCandidate.trim();
|
|
36
|
+
if (!providerId || !modelId) {
|
|
37
|
+
throw new ConfigApplyError("schema", `Invalid model reference: ${rawRef}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (modelsByProvider.has(providerId)) {
|
|
41
|
+
return { providerId, modelId };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return { providerId: defaultProvider, modelId: trimmed };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function loadProviderModelMap(config: AgentMockingbirdConfig) {
|
|
48
|
+
try {
|
|
49
|
+
const client = createOpencodeClientFromConnection({
|
|
50
|
+
baseUrl: config.runtime.opencode.baseUrl,
|
|
51
|
+
directory: config.runtime.opencode.directory,
|
|
52
|
+
});
|
|
53
|
+
const payload = unwrapSdkData<ConfigProvidersResponse>(
|
|
54
|
+
await client.config.providers({
|
|
55
|
+
responseStyle: "data",
|
|
56
|
+
throwOnError: true,
|
|
57
|
+
signal: AbortSignal.timeout(config.runtime.opencode.timeoutMs),
|
|
58
|
+
}),
|
|
59
|
+
);
|
|
60
|
+
const modelsByProvider = new Map<string, Set<string>>();
|
|
61
|
+
let modelCount = 0;
|
|
62
|
+
for (const provider of payload.providers) {
|
|
63
|
+
const modelSet = new Set<string>();
|
|
64
|
+
for (const [modelKey, model] of Object.entries(provider.models)) {
|
|
65
|
+
modelSet.add((model.id ?? modelKey).trim());
|
|
66
|
+
modelCount += 1;
|
|
67
|
+
}
|
|
68
|
+
modelsByProvider.set(provider.id, modelSet);
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
modelsByProvider,
|
|
72
|
+
providerCount: payload.providers.length,
|
|
73
|
+
modelCount,
|
|
74
|
+
};
|
|
75
|
+
} catch (error) {
|
|
76
|
+
const message = error instanceof Error ? error.message : "Failed to load OpenCode providers";
|
|
77
|
+
throw new ConfigApplyError("semantic", message);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function assertModelAvailable(
|
|
82
|
+
modelsByProvider: Map<string, Set<string>>,
|
|
83
|
+
providerId: string,
|
|
84
|
+
modelId: string,
|
|
85
|
+
field: string,
|
|
86
|
+
) {
|
|
87
|
+
const providerModels = modelsByProvider.get(providerId);
|
|
88
|
+
if (!providerModels) {
|
|
89
|
+
throw new ConfigApplyError("semantic", `${field} references unknown provider: ${providerId}`);
|
|
90
|
+
}
|
|
91
|
+
if (!providerModels.has(modelId)) {
|
|
92
|
+
throw new ConfigApplyError("semantic", `${field} references unknown model: ${providerId}/${modelId}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function runSemanticValidation(config: AgentMockingbirdConfig): Promise<ConfigSemanticSummary> {
|
|
97
|
+
const providerMap = await loadProviderModelMap(config);
|
|
98
|
+
const primaryRef = resolveModelRefForValidation(
|
|
99
|
+
config.runtime.opencode.modelId,
|
|
100
|
+
config.runtime.opencode.providerId,
|
|
101
|
+
providerMap.modelsByProvider,
|
|
102
|
+
);
|
|
103
|
+
assertModelAvailable(providerMap.modelsByProvider, primaryRef.providerId, primaryRef.modelId, "runtime.opencode.modelId");
|
|
104
|
+
|
|
105
|
+
const smallModelRef = resolveModelRefForValidation(
|
|
106
|
+
config.runtime.opencode.smallModel,
|
|
107
|
+
config.runtime.opencode.providerId,
|
|
108
|
+
providerMap.modelsByProvider,
|
|
109
|
+
);
|
|
110
|
+
assertModelAvailable(
|
|
111
|
+
providerMap.modelsByProvider,
|
|
112
|
+
smallModelRef.providerId,
|
|
113
|
+
smallModelRef.modelId,
|
|
114
|
+
"runtime.opencode.smallModel",
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
for (const fallbackRef of config.runtime.opencode.fallbackModels) {
|
|
118
|
+
const parsedFallback = resolveModelRefForValidation(
|
|
119
|
+
fallbackRef,
|
|
120
|
+
config.runtime.opencode.providerId,
|
|
121
|
+
providerMap.modelsByProvider,
|
|
122
|
+
);
|
|
123
|
+
assertModelAvailable(
|
|
124
|
+
providerMap.modelsByProvider,
|
|
125
|
+
parsedFallback.providerId,
|
|
126
|
+
parsedFallback.modelId,
|
|
127
|
+
"runtime.opencode.fallbackModels",
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (config.runtime.opencode.imageModel?.trim()) {
|
|
132
|
+
const imageModelRef = resolveModelRefForValidation(
|
|
133
|
+
config.runtime.opencode.imageModel,
|
|
134
|
+
config.runtime.opencode.providerId,
|
|
135
|
+
providerMap.modelsByProvider,
|
|
136
|
+
);
|
|
137
|
+
assertModelAvailable(
|
|
138
|
+
providerMap.modelsByProvider,
|
|
139
|
+
imageModelRef.providerId,
|
|
140
|
+
imageModelRef.modelId,
|
|
141
|
+
"runtime.opencode.imageModel",
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
providerCount: providerMap.providerCount,
|
|
147
|
+
modelCount: providerMap.modelCount,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
import { resolveExampleConfigPath } from "./testFixtures";
|
|
7
|
+
|
|
8
|
+
test("applyConfigPatch skips OpenCode semantic validation for memory-only changes", async () => {
|
|
9
|
+
const tempRoot = mkdtempSync(path.join(tmpdir(), "agent-mockingbird-config-service-"));
|
|
10
|
+
const configPath = path.join(tempRoot, "agent-mockingbird.config.json");
|
|
11
|
+
const workspacePath = path.join(tempRoot, "workspace");
|
|
12
|
+
const fixturePath = resolveExampleConfigPath();
|
|
13
|
+
const raw = JSON.parse(readFileSync(fixturePath, "utf8")) as {
|
|
14
|
+
runtime?: {
|
|
15
|
+
configPolicy?: Record<string, unknown>;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const previousConfigPath = process.env.AGENT_MOCKINGBIRD_CONFIG_PATH;
|
|
20
|
+
const previousWorkspacePath = process.env.AGENT_MOCKINGBIRD_MEMORY_WORKSPACE_DIR;
|
|
21
|
+
const previousNodeEnv = process.env.NODE_ENV;
|
|
22
|
+
const previousFetch = globalThis.fetch;
|
|
23
|
+
const blockingFetch = Object.assign(
|
|
24
|
+
(input: Parameters<typeof fetch>[0], init?: Parameters<typeof fetch>[1]) => {
|
|
25
|
+
void input;
|
|
26
|
+
void init;
|
|
27
|
+
throw new Error("semantic validation should not call fetch for memory-only patches");
|
|
28
|
+
},
|
|
29
|
+
previousFetch,
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
writeFileSync(configPath, JSON.stringify(raw, null, 2), "utf8");
|
|
33
|
+
process.env.NODE_ENV = "test";
|
|
34
|
+
process.env.AGENT_MOCKINGBIRD_CONFIG_PATH = configPath;
|
|
35
|
+
process.env.AGENT_MOCKINGBIRD_MEMORY_WORKSPACE_DIR = workspacePath;
|
|
36
|
+
globalThis.fetch = blockingFetch;
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const { getConfigSnapshot, applyConfigPatch } = await import("./service");
|
|
40
|
+
getConfigSnapshot();
|
|
41
|
+
const result = await applyConfigPatch({
|
|
42
|
+
runSmokeTest: false,
|
|
43
|
+
patch: {
|
|
44
|
+
runtime: {
|
|
45
|
+
memory: {
|
|
46
|
+
enabled: true,
|
|
47
|
+
embedProvider: "ollama",
|
|
48
|
+
embedModel: "granite-embedding:278m",
|
|
49
|
+
ollamaBaseUrl: "http://172.16.1.100:11434",
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
expect(result.snapshot.config.runtime.memory.ollamaBaseUrl).toBe("http://172.16.1.100:11434");
|
|
56
|
+
} finally {
|
|
57
|
+
globalThis.fetch = previousFetch;
|
|
58
|
+
if (previousConfigPath === undefined) {
|
|
59
|
+
delete process.env.AGENT_MOCKINGBIRD_CONFIG_PATH;
|
|
60
|
+
} else {
|
|
61
|
+
process.env.AGENT_MOCKINGBIRD_CONFIG_PATH = previousConfigPath;
|
|
62
|
+
}
|
|
63
|
+
if (previousWorkspacePath === undefined) {
|
|
64
|
+
delete process.env.AGENT_MOCKINGBIRD_MEMORY_WORKSPACE_DIR;
|
|
65
|
+
} else {
|
|
66
|
+
process.env.AGENT_MOCKINGBIRD_MEMORY_WORKSPACE_DIR = previousWorkspacePath;
|
|
67
|
+
}
|
|
68
|
+
if (previousNodeEnv === undefined) {
|
|
69
|
+
delete process.env.NODE_ENV;
|
|
70
|
+
} else {
|
|
71
|
+
process.env.NODE_ENV = previousNodeEnv;
|
|
72
|
+
}
|
|
73
|
+
rmSync(tempRoot, { recursive: true, force: true });
|
|
74
|
+
}
|
|
75
|
+
});
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import {
|
|
2
|
+
assertConfigPolicyAllows,
|
|
3
|
+
evaluateConfigPolicyForPatch,
|
|
4
|
+
evaluateConfigPolicyForReplace,
|
|
5
|
+
} from "./policy";
|
|
6
|
+
import type { AgentMockingbirdConfig } from "./schema";
|
|
7
|
+
import { runSemanticValidation } from "./semantic";
|
|
8
|
+
import { runSmokeTest } from "./smoke";
|
|
9
|
+
import {
|
|
10
|
+
assertExpectedHashMatches,
|
|
11
|
+
ensureConfigSnapshot,
|
|
12
|
+
getConfig,
|
|
13
|
+
getConfigSnapshot,
|
|
14
|
+
mergeConfigPatch,
|
|
15
|
+
parseConfig,
|
|
16
|
+
persistConfigSnapshot,
|
|
17
|
+
} from "./store";
|
|
18
|
+
import {
|
|
19
|
+
ConfigApplyError,
|
|
20
|
+
type ConfigPolicySummary,
|
|
21
|
+
type ConfigSemanticSummary,
|
|
22
|
+
type ConfigSmokeTestSummary,
|
|
23
|
+
} from "./types";
|
|
24
|
+
|
|
25
|
+
export {
|
|
26
|
+
ConfigApplyError,
|
|
27
|
+
type ApplyConfigResult,
|
|
28
|
+
} from "./types";
|
|
29
|
+
|
|
30
|
+
async function applyCandidateConfig(
|
|
31
|
+
input: {
|
|
32
|
+
currentPath: string;
|
|
33
|
+
currentConfig: AgentMockingbirdConfig;
|
|
34
|
+
candidate: AgentMockingbirdConfig;
|
|
35
|
+
runSemanticValidation: boolean;
|
|
36
|
+
runSmokeValidation: boolean;
|
|
37
|
+
autoRollbackOnFailure: boolean;
|
|
38
|
+
},
|
|
39
|
+
): Promise<{
|
|
40
|
+
snapshot: ReturnType<typeof persistConfigSnapshot>;
|
|
41
|
+
semantic: ConfigSemanticSummary;
|
|
42
|
+
smokeTest: ConfigSmokeTestSummary | null;
|
|
43
|
+
}> {
|
|
44
|
+
const semantic = input.runSemanticValidation
|
|
45
|
+
? await runSemanticValidation(input.candidate)
|
|
46
|
+
: { providerCount: 0, modelCount: 0 };
|
|
47
|
+
if (!input.runSmokeValidation) {
|
|
48
|
+
const snapshot = persistConfigSnapshot(input.currentPath, input.candidate);
|
|
49
|
+
return { snapshot, semantic, smokeTest: null };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!input.autoRollbackOnFailure) {
|
|
53
|
+
const smokeTest = await runSmokeTest(input.candidate);
|
|
54
|
+
const snapshot = persistConfigSnapshot(input.currentPath, input.candidate);
|
|
55
|
+
return { snapshot, semantic, smokeTest };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const attemptedSnapshot = persistConfigSnapshot(input.currentPath, input.candidate);
|
|
59
|
+
try {
|
|
60
|
+
const smokeTest = await runSmokeTest(input.candidate);
|
|
61
|
+
return { snapshot: attemptedSnapshot, semantic, smokeTest };
|
|
62
|
+
} catch (error) {
|
|
63
|
+
let rollbackSnapshot: ReturnType<typeof persistConfigSnapshot> | null = null;
|
|
64
|
+
try {
|
|
65
|
+
rollbackSnapshot = persistConfigSnapshot(input.currentPath, input.currentConfig);
|
|
66
|
+
} catch (rollbackError) {
|
|
67
|
+
const rollbackMessage =
|
|
68
|
+
rollbackError instanceof Error ? rollbackError.message : "Failed to rollback config";
|
|
69
|
+
throw new ConfigApplyError("rollback", rollbackMessage, {
|
|
70
|
+
attemptedHash: attemptedSnapshot.hash,
|
|
71
|
+
originalError: error instanceof Error ? error.message : "Smoke test failed",
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (error instanceof ConfigApplyError) {
|
|
76
|
+
throw new ConfigApplyError(error.stage, error.message, {
|
|
77
|
+
...(error.details ?? {}),
|
|
78
|
+
rolledBack: true,
|
|
79
|
+
attemptedHash: attemptedSnapshot.hash,
|
|
80
|
+
restoredHash: rollbackSnapshot.hash,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const message = error instanceof Error ? error.message : "Smoke test failed";
|
|
85
|
+
throw new ConfigApplyError("smoke", message, {
|
|
86
|
+
rolledBack: true,
|
|
87
|
+
attemptedHash: attemptedSnapshot.hash,
|
|
88
|
+
restoredHash: rollbackSnapshot.hash,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export { ensureConfigSnapshot as ensureConfigFile, getConfigSnapshot, getConfig };
|
|
94
|
+
|
|
95
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
96
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function shouldRunSemanticValidation(current: AgentMockingbirdConfig, candidate: AgentMockingbirdConfig) {
|
|
100
|
+
return JSON.stringify(current.runtime.opencode) !== JSON.stringify(candidate.runtime.opencode);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function applyConfigPatch(input: {
|
|
104
|
+
patch: unknown;
|
|
105
|
+
expectedHash?: string;
|
|
106
|
+
runSmokeTest?: boolean;
|
|
107
|
+
safeMode?: boolean;
|
|
108
|
+
}) {
|
|
109
|
+
const current = ensureConfigSnapshot();
|
|
110
|
+
if (!isPlainObject(input.patch)) {
|
|
111
|
+
throw new ConfigApplyError("request", "patch must be an object");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
let policy: ConfigPolicySummary | null = null;
|
|
115
|
+
if (input.safeMode) {
|
|
116
|
+
policy = evaluateConfigPolicyForPatch(current.config, input.patch);
|
|
117
|
+
assertConfigPolicyAllows(policy);
|
|
118
|
+
if (policy.requireExpectedHash && !input.expectedHash) {
|
|
119
|
+
throw new ConfigApplyError("request", "expectedHash is required in safe mode");
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
assertExpectedHashMatches(current.hash, input.expectedHash);
|
|
123
|
+
|
|
124
|
+
const merged = mergeConfigPatch(current.config, input.patch);
|
|
125
|
+
const candidate = parseConfig(merged);
|
|
126
|
+
if (input.safeMode) {
|
|
127
|
+
policy = evaluateConfigPolicyForReplace(current.config, candidate);
|
|
128
|
+
assertConfigPolicyAllows(policy);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const runSmokeValidation = policy?.requireSmokeTest ? true : input.runSmokeTest !== false;
|
|
132
|
+
const runSemanticValidation = shouldRunSemanticValidation(current.config, candidate);
|
|
133
|
+
const validated = await applyCandidateConfig({
|
|
134
|
+
currentPath: current.path,
|
|
135
|
+
currentConfig: current.config,
|
|
136
|
+
candidate,
|
|
137
|
+
runSemanticValidation,
|
|
138
|
+
runSmokeValidation,
|
|
139
|
+
autoRollbackOnFailure: policy?.autoRollbackOnFailure ?? false,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
snapshot: validated.snapshot,
|
|
144
|
+
semantic: validated.semantic,
|
|
145
|
+
smokeTest: validated.smokeTest,
|
|
146
|
+
policy,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export async function replaceConfig(input: {
|
|
151
|
+
config: unknown;
|
|
152
|
+
expectedHash?: string;
|
|
153
|
+
runSmokeTest?: boolean;
|
|
154
|
+
safeMode?: boolean;
|
|
155
|
+
}) {
|
|
156
|
+
const current = ensureConfigSnapshot();
|
|
157
|
+
const candidate = parseConfig(input.config);
|
|
158
|
+
let policy: ConfigPolicySummary | null = null;
|
|
159
|
+
if (input.safeMode) {
|
|
160
|
+
policy = evaluateConfigPolicyForReplace(current.config, candidate);
|
|
161
|
+
assertConfigPolicyAllows(policy);
|
|
162
|
+
if (policy.requireExpectedHash && !input.expectedHash) {
|
|
163
|
+
throw new ConfigApplyError("request", "expectedHash is required in safe mode");
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
assertExpectedHashMatches(current.hash, input.expectedHash);
|
|
167
|
+
|
|
168
|
+
const runSmokeValidation = policy?.requireSmokeTest ? true : input.runSmokeTest !== false;
|
|
169
|
+
const runSemanticValidation = shouldRunSemanticValidation(current.config, candidate);
|
|
170
|
+
const validated = await applyCandidateConfig({
|
|
171
|
+
currentPath: current.path,
|
|
172
|
+
currentConfig: current.config,
|
|
173
|
+
candidate,
|
|
174
|
+
runSemanticValidation,
|
|
175
|
+
runSmokeValidation,
|
|
176
|
+
autoRollbackOnFailure: policy?.autoRollbackOnFailure ?? false,
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
snapshot: validated.snapshot,
|
|
181
|
+
semantic: validated.semantic,
|
|
182
|
+
smokeTest: validated.smokeTest,
|
|
183
|
+
policy,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export async function applyConfigPatchSafe(input: {
|
|
188
|
+
patch: unknown;
|
|
189
|
+
expectedHash?: string;
|
|
190
|
+
runSmokeTest?: boolean;
|
|
191
|
+
}) {
|
|
192
|
+
return applyConfigPatch({
|
|
193
|
+
...input,
|
|
194
|
+
safeMode: true,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export async function replaceConfigSafe(input: {
|
|
199
|
+
config: unknown;
|
|
200
|
+
expectedHash?: string;
|
|
201
|
+
runSmokeTest?: boolean;
|
|
202
|
+
}) {
|
|
203
|
+
return replaceConfig({
|
|
204
|
+
...input,
|
|
205
|
+
safeMode: true,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { Part, Session } from "@opencode-ai/sdk/client";
|
|
2
|
+
|
|
3
|
+
import type { AgentMockingbirdConfig } from "./schema";
|
|
4
|
+
import { ConfigApplyError, type ConfigSmokeTestSummary } from "./types";
|
|
5
|
+
import { createOpencodeClientFromConnection, unwrapSdkData } from "../opencode/client";
|
|
6
|
+
|
|
7
|
+
function extractAssistantText(parts: Array<Part>) {
|
|
8
|
+
return parts
|
|
9
|
+
.filter((part): part is Extract<Part, { type: "text" }> => part.type === "text")
|
|
10
|
+
.map(part => part.text.trim())
|
|
11
|
+
.filter(Boolean)
|
|
12
|
+
.join("\n\n");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function runSmokeTest(config: AgentMockingbirdConfig): Promise<ConfigSmokeTestSummary> {
|
|
16
|
+
const client = createOpencodeClientFromConnection({
|
|
17
|
+
baseUrl: config.runtime.opencode.baseUrl,
|
|
18
|
+
directory: config.runtime.opencode.directory,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
let expectedPattern: RegExp;
|
|
22
|
+
try {
|
|
23
|
+
expectedPattern = new RegExp(config.runtime.smokeTest.expectedResponsePattern, "i");
|
|
24
|
+
} catch {
|
|
25
|
+
throw new ConfigApplyError("schema", "runtime.smokeTest.expectedResponsePattern is not a valid regex");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const session = unwrapSdkData<Session>(
|
|
30
|
+
await client.session.create({
|
|
31
|
+
body: { title: "agent-mockingbird-config-smoke" },
|
|
32
|
+
responseStyle: "data",
|
|
33
|
+
throwOnError: true,
|
|
34
|
+
signal: AbortSignal.timeout(config.runtime.opencode.timeoutMs),
|
|
35
|
+
}),
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const response = unwrapSdkData<{ info: { role: string }; parts: Array<Part> }>(
|
|
39
|
+
await client.session.prompt({
|
|
40
|
+
path: { id: session.id },
|
|
41
|
+
body: {
|
|
42
|
+
model: {
|
|
43
|
+
providerID: config.runtime.opencode.providerId,
|
|
44
|
+
modelID: config.runtime.opencode.modelId,
|
|
45
|
+
},
|
|
46
|
+
parts: [{ type: "text", text: config.runtime.smokeTest.prompt }],
|
|
47
|
+
},
|
|
48
|
+
responseStyle: "data",
|
|
49
|
+
throwOnError: true,
|
|
50
|
+
signal: AbortSignal.timeout(config.runtime.opencode.promptTimeoutMs),
|
|
51
|
+
}),
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const text = extractAssistantText(response.parts);
|
|
55
|
+
if (!text) {
|
|
56
|
+
throw new ConfigApplyError("smoke", "Smoke test returned no assistant text response");
|
|
57
|
+
}
|
|
58
|
+
if (!expectedPattern.test(text)) {
|
|
59
|
+
throw new ConfigApplyError(
|
|
60
|
+
"smoke",
|
|
61
|
+
`Smoke test response did not match expected pattern: ${config.runtime.smokeTest.expectedResponsePattern}`,
|
|
62
|
+
{ responseText: text },
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
sessionId: session.id,
|
|
68
|
+
responseText: text,
|
|
69
|
+
};
|
|
70
|
+
} catch (error) {
|
|
71
|
+
if (error instanceof ConfigApplyError) {
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
const message = error instanceof Error ? error.message : "Smoke test failed";
|
|
75
|
+
throw new ConfigApplyError("smoke", message);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import { parseConfig } from "./store";
|
|
6
|
+
import { resolveExampleConfigPath } from "./testFixtures";
|
|
7
|
+
import { ConfigApplyError } from "./types";
|
|
8
|
+
|
|
9
|
+
test("parseConfig reports a clear error when AGENT_MOCKINGBIRD_CONFIG_PATH points to OpenCode config.json", () => {
|
|
10
|
+
expect(() =>
|
|
11
|
+
parseConfig({
|
|
12
|
+
$schema: "https://opencode.ai/config.json",
|
|
13
|
+
model: "anthropic/claude-opus-4-5",
|
|
14
|
+
}),
|
|
15
|
+
).toThrowError(
|
|
16
|
+
new ConfigApplyError(
|
|
17
|
+
"schema",
|
|
18
|
+
"Config file appears to be OpenCode config.json, not Agent Mockingbird config. Set AGENT_MOCKINGBIRD_CONFIG_PATH to an Agent Mockingbird config file (default: ./data/agent-mockingbird.config.json).",
|
|
19
|
+
),
|
|
20
|
+
);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("parseConfig uses AGENT_MOCKINGBIRD_OPENCODE_* env vars as runtime fallbacks when fields are unset", () => {
|
|
24
|
+
const previousModelId = process.env.AGENT_MOCKINGBIRD_OPENCODE_MODEL_ID;
|
|
25
|
+
process.env.AGENT_MOCKINGBIRD_OPENCODE_MODEL_ID = "env-only-model";
|
|
26
|
+
try {
|
|
27
|
+
const filePath = resolveExampleConfigPath();
|
|
28
|
+
const raw = JSON.parse(readFileSync(filePath, "utf8")) as {
|
|
29
|
+
runtime?: { opencode?: Record<string, unknown> };
|
|
30
|
+
};
|
|
31
|
+
if (!raw.runtime?.opencode) {
|
|
32
|
+
throw new Error("Test fixture missing runtime.opencode");
|
|
33
|
+
}
|
|
34
|
+
delete raw.runtime.opencode.modelId;
|
|
35
|
+
expect(parseConfig(raw).runtime.opencode.modelId).toBe("env-only-model");
|
|
36
|
+
} finally {
|
|
37
|
+
if (previousModelId === undefined) {
|
|
38
|
+
delete process.env.AGENT_MOCKINGBIRD_OPENCODE_MODEL_ID;
|
|
39
|
+
} else {
|
|
40
|
+
process.env.AGENT_MOCKINGBIRD_OPENCODE_MODEL_ID = previousModelId;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("parseConfig aligns runtime workspace paths to workspace.pinnedDirectory", () => {
|
|
46
|
+
const filePath = resolveExampleConfigPath();
|
|
47
|
+
const raw = JSON.parse(readFileSync(filePath, "utf8")) as {
|
|
48
|
+
workspace?: Record<string, unknown>;
|
|
49
|
+
};
|
|
50
|
+
if (!raw.workspace) {
|
|
51
|
+
throw new Error("Test fixture missing workspace settings");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
raw.workspace.pinnedDirectory = "./custom-workspace";
|
|
55
|
+
const parsed = parseConfig(raw);
|
|
56
|
+
const expected = path.resolve(process.cwd(), "custom-workspace");
|
|
57
|
+
expect(parsed.workspace.pinnedDirectory).toBe(expected);
|
|
58
|
+
expect(parsed.runtime.opencode.directory).toBe(expected);
|
|
59
|
+
expect(parsed.runtime.memory.workspaceDir).toBe(expected);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("parseConfig ignores legacy mismatched runtime workspace fields in favor of workspace.pinnedDirectory", () => {
|
|
63
|
+
const filePath = resolveExampleConfigPath();
|
|
64
|
+
const raw = JSON.parse(readFileSync(filePath, "utf8")) as {
|
|
65
|
+
workspace?: Record<string, unknown>;
|
|
66
|
+
runtime?: { opencode?: Record<string, unknown>; memory?: Record<string, unknown> };
|
|
67
|
+
};
|
|
68
|
+
if (!raw.workspace || !raw.runtime?.opencode || !raw.runtime.memory) {
|
|
69
|
+
throw new Error("Test fixture missing runtime workspace settings");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
raw.workspace.pinnedDirectory = "/tmp/pinned-workspace";
|
|
73
|
+
raw.runtime.opencode.directory = "/tmp/opencode-workspace";
|
|
74
|
+
raw.runtime.memory.workspaceDir = "/tmp/memory-workspace";
|
|
75
|
+
const parsed = parseConfig(raw);
|
|
76
|
+
expect(parsed.workspace.pinnedDirectory).toBe("/tmp/pinned-workspace");
|
|
77
|
+
expect(parsed.runtime.opencode.directory).toBe("/tmp/pinned-workspace");
|
|
78
|
+
expect(parsed.runtime.memory.workspaceDir).toBe("/tmp/pinned-workspace");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("example config no longer ships heartbeat config on the default build agent", () => {
|
|
82
|
+
const filePath = resolveExampleConfigPath();
|
|
83
|
+
const raw = JSON.parse(readFileSync(filePath, "utf8")) as {
|
|
84
|
+
ui?: { agentTypes?: Array<{ id?: string; heartbeat?: unknown }> };
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const buildAgent = raw.ui?.agentTypes?.find(agent => agent.id === "build");
|
|
88
|
+
expect(buildAgent).toBeDefined();
|
|
89
|
+
expect(buildAgent?.heartbeat).toBeUndefined();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("parseConfig migrates legacy agent heartbeat blocks into runtime.heartbeat", () => {
|
|
93
|
+
const filePath = resolveExampleConfigPath();
|
|
94
|
+
const raw = JSON.parse(readFileSync(filePath, "utf8")) as {
|
|
95
|
+
runtime?: Record<string, unknown>;
|
|
96
|
+
ui?: { agentTypes?: Array<Record<string, unknown>> };
|
|
97
|
+
};
|
|
98
|
+
if (!raw.runtime || !raw.ui?.agentTypes?.[0]) {
|
|
99
|
+
throw new Error("Test fixture missing runtime/ui.agentTypes");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
delete raw.runtime.heartbeat;
|
|
103
|
+
raw.ui.agentTypes[0] = {
|
|
104
|
+
...raw.ui.agentTypes[0],
|
|
105
|
+
id: "build",
|
|
106
|
+
model: "opencode/legacy-heartbeat-model",
|
|
107
|
+
heartbeat: {
|
|
108
|
+
enabled: true,
|
|
109
|
+
interval: "45m",
|
|
110
|
+
prompt: "legacy heartbeat prompt",
|
|
111
|
+
ackMaxChars: 123,
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const parsed = parseConfig(raw);
|
|
116
|
+
expect(parsed.runtime.heartbeat.agentId).toBe("build");
|
|
117
|
+
expect(parsed.runtime.heartbeat.model).toBe("opencode/legacy-heartbeat-model");
|
|
118
|
+
expect(parsed.runtime.heartbeat.interval).toBe("45m");
|
|
119
|
+
expect(parsed.runtime.heartbeat.prompt).toBe("legacy heartbeat prompt");
|
|
120
|
+
expect(parsed.runtime.heartbeat.ackMaxChars).toBe(123);
|
|
121
|
+
expect(parsed.ui.agentTypes[0]?.id).toBe("build");
|
|
122
|
+
expect("heartbeat" in (parsed.ui.agentTypes[0] ?? {})).toBe(false);
|
|
123
|
+
});
|