Compare commits

...

2 Commits

Author SHA1 Message Date
cline-test 3e977874bd feat(hooks): Get the model identifier string right. 2026-02-03 10:11:24 -08:00
cline-test 696e580d21 feat(hooks): Add model name to hooks JSON. 2026-02-03 10:11:24 -08:00
9 changed files with 508 additions and 16 deletions
+3
View File
@@ -14,6 +14,9 @@ message HookInput {
string task_id = 4;
repeated string workspace_roots = 5;
string user_id = 6;
// The configured API provider + configured model id, formatted as "${apiProvider}:${apiModelId}".
// Examples: "anthropic:claude-sonnet-4-20250514", "cline:openai/gpt-5.2"
string model_id = 7;
oneof data {
PreToolUseData pre_tool_use = 10;
PostToolUseData post_tool_use = 11;
+362 -1
View File
@@ -47,6 +47,24 @@ describe("Hook System", () => {
}
return undefined
},
getGlobalSettingsKey: (key: string) => {
if (key === "mode") {
return "act"
}
if (key === "actModeApiProvider") {
return "anthropic"
}
if (key === "actModeApiModelId") {
return "claude-sonnet-4-20250514"
}
if (key === "planModeApiProvider") {
return "anthropic"
}
if (key === "planModeApiModelId") {
return "claude-sonnet-4-20250514"
}
return undefined
},
} as any)
// Reset hook discovery cache for clean test state
@@ -63,7 +81,7 @@ describe("Hook System", () => {
try {
await fs.rm(tempDir, { recursive: true, force: true })
} catch (error) {
} catch (_error) {
// Ignore cleanup errors
}
})
@@ -419,6 +437,349 @@ console.log(JSON.stringify({
})
})
describe("Model ID in Hook Input", () => {
const restubStateManagerGet = (overrides: {
mode?: "plan" | "act"
actModeApiProvider?: string
actModeApiModelId?: string
actModeOpenRouterModelId?: string
actModeOpenAiModelId?: string
actModeOllamaModelId?: string
planModeApiProvider?: string
planModeApiModelId?: string
planModeOpenRouterModelId?: string
planModeOpenAiModelId?: string
}): void => {
// Replace only this stub; avoid sandbox.restore() which can unintentionally
// remove unrelated stubs/mocks within the test.
;(StateManager.get as sinon.SinonStub).restore()
sandbox.stub(StateManager, "get").returns({
getGlobalStateKey: (key: string) => {
if (key === "workspaceRoots") {
return [{ path: tempDir }]
}
if (key === "primaryRootIndex") {
return 0
}
return undefined
},
getGlobalSettingsKey: (key: string) => {
if (key === "mode") return overrides.mode ?? "act"
if (key === "actModeApiProvider") return overrides.actModeApiProvider ?? "anthropic"
if (key === "actModeApiModelId") return overrides.actModeApiModelId ?? "claude-sonnet-4-20250514"
if (key === "actModeOpenRouterModelId") return overrides.actModeOpenRouterModelId
if (key === "actModeOpenAiModelId") return overrides.actModeOpenAiModelId
if (key === "actModeOllamaModelId") return overrides.actModeOllamaModelId
if (key === "planModeApiProvider") return overrides.planModeApiProvider ?? "anthropic"
if (key === "planModeApiModelId") return overrides.planModeApiModelId ?? "claude-sonnet-4-20250514"
if (key === "planModeOpenRouterModelId") return overrides.planModeOpenRouterModelId
if (key === "planModeOpenAiModelId") return overrides.planModeOpenAiModelId
return undefined
},
} as any)
}
it("should include modelId in hook input with format provider:modelId", async () => {
const hookPath = path.join(tempDir, ".clinerules", "hooks", "PreToolUse")
const hookScript = `#!/usr/bin/env node
const input = JSON.parse(require('fs').readFileSync(0, 'utf-8'));
console.log(JSON.stringify({
cancel: false,
contextModification: "Model ID: " + input.modelId
}))`
await writeHookScript(hookPath, hookScript)
const factory = new HookFactory()
const runner = await factory.create("PreToolUse")
const result = await runner.run({
taskId: "test-task",
preToolUse: {
toolName: "test_tool",
parameters: {},
},
})
result.cancel.should.be.false()
// Default mock returns anthropic:claude-sonnet-4-20250514
result.contextModification!.should.equal("Model ID: anthropic:claude-sonnet-4-20250514")
})
it("should include modelId with cline provider format (cline:anthropic/claude-sonnet-4.5)", async () => {
restubStateManagerGet({
mode: "act",
actModeApiProvider: "cline",
actModeApiModelId: "claude-sonnet-4-20250514", // should not be used for cline modelId display
actModeOpenRouterModelId: "anthropic/claude-sonnet-4.5",
planModeApiProvider: "cline",
planModeApiModelId: "claude-sonnet-4-20250514",
planModeOpenRouterModelId: "anthropic/claude-sonnet-4.5",
})
const hookPath = path.join(tempDir, ".clinerules", "hooks", "PreToolUse")
const hookScript = `#!/usr/bin/env node
const input = JSON.parse(require('fs').readFileSync(0, 'utf-8'));
console.log(JSON.stringify({
cancel: false,
contextModification: "Model ID: " + input.modelId
}))`
await writeHookScript(hookPath, hookScript)
const factory = new HookFactory()
const runner = await factory.create("PreToolUse")
const result = await runner.run({
taskId: "test-task",
preToolUse: {
toolName: "test_tool",
parameters: {},
},
})
result.cancel.should.be.false()
result.contextModification!.should.equal("Model ID: cline:anthropic/claude-sonnet-4.5")
})
it("should use plan mode provider/model when in plan mode", async () => {
restubStateManagerGet({
mode: "plan",
actModeApiProvider: "anthropic",
actModeApiModelId: "claude-sonnet-4-20250514",
planModeApiProvider: "cline",
planModeApiModelId: "claude-opus-4-5-20251101", // should not be used for cline modelId display
planModeOpenRouterModelId: "openai/gpt-5.2",
})
const hookPath = path.join(tempDir, ".clinerules", "hooks", "PreToolUse")
const hookScript = `#!/usr/bin/env node
const input = JSON.parse(require('fs').readFileSync(0, 'utf-8'));
console.log(JSON.stringify({
cancel: false,
contextModification: "Model ID: " + input.modelId
}))`
await writeHookScript(hookPath, hookScript)
const factory = new HookFactory()
const runner = await factory.create("PreToolUse")
const result = await runner.run({
taskId: "test-task",
preToolUse: {
toolName: "test_tool",
parameters: {},
},
})
result.cancel.should.be.false()
// Should use plan mode provider/model since mode is "plan"
result.contextModification!.should.equal("Model ID: cline:openai/gpt-5.2")
})
it("should fall back to empty model for cline if openRouterModelId is missing", async () => {
restubStateManagerGet({
mode: "act",
actModeApiProvider: "cline",
actModeApiModelId: "claude-opus-4-5-20251101",
// intentionally missing actModeOpenRouterModelId
})
const hookPath = path.join(tempDir, ".clinerules", "hooks", "PreToolUse")
const hookScript = `#!/usr/bin/env node
const input = JSON.parse(require('fs').readFileSync(0, 'utf-8'));
console.log(JSON.stringify({ cancel: false, contextModification: "Model ID: " + input.modelId }))`
await writeHookScript(hookPath, hookScript)
const factory = new HookFactory()
const runner = await factory.create("PreToolUse")
const result = await runner.run({
taskId: "test-task",
preToolUse: {
toolName: "test_tool",
parameters: {},
},
})
result.cancel.should.be.false()
result.contextModification!.should.equal("Model ID: cline:")
})
it("should use openai-compat prefix when provider is openai", async () => {
restubStateManagerGet({
mode: "act",
actModeApiProvider: "openai",
actModeApiModelId: "claude-opus-4-5-20251101", // should not be used when OpenAI model id is present
actModeOpenAiModelId: "gpt-4.1",
planModeApiProvider: "openai",
planModeApiModelId: "claude-opus-4-5-20251101",
planModeOpenAiModelId: "gpt-4.1",
})
const hookPath = path.join(tempDir, ".clinerules", "hooks", "PreToolUse")
const hookScript = `#!/usr/bin/env node
const input = JSON.parse(require('fs').readFileSync(0, 'utf-8'));
console.log(JSON.stringify({
cancel: false,
contextModification: "Model ID: " + input.modelId
}))`
await writeHookScript(hookPath, hookScript)
const factory = new HookFactory()
const runner = await factory.create("PreToolUse")
const result = await runner.run({
taskId: "test-task",
preToolUse: {
toolName: "test_tool",
parameters: {},
},
})
result.cancel.should.be.false()
result.contextModification!.should.equal("Model ID: openai-compat:gpt-4.1")
})
it("should not use stale actModeApiModelId when switching to openai (uses actModeOpenAiModelId)", async () => {
// This reproduces a real-world scenario:
// 1) user previously used Anthropic provider (actModeApiModelId set to a claude model)
// 2) user switches to OpenAI-compatible provider and selects a GPT model
// The hook modelId should be openai-compat:<openAiModelId>, NOT openai-compat:<stale actModeApiModelId>
restubStateManagerGet({
mode: "act",
actModeApiProvider: "openai",
actModeApiModelId: "claude-opus-4-5-20251101", // stale value from prior provider
actModeOpenAiModelId: "gpt-4.1",
})
const hookPath = path.join(tempDir, ".clinerules", "hooks", "PreToolUse")
const hookScript = `#!/usr/bin/env node
const input = JSON.parse(require('fs').readFileSync(0, 'utf-8'));
console.log(JSON.stringify({ cancel: false, contextModification: "Model ID: " + input.modelId }))`
await writeHookScript(hookPath, hookScript)
const factory = new HookFactory()
const runner = await factory.create("PreToolUse")
const result = await runner.run({
taskId: "test-task",
preToolUse: { toolName: "test_tool", parameters: {} },
})
result.cancel.should.be.false()
result.contextModification!.should.equal("Model ID: openai-compat:gpt-4.1")
})
it("should use default model for openai-native when apiModelId is from different provider", async () => {
// Simulates switching from Anthropic to openai-native:
// - actModeApiModelId still has claude model (not valid for openai-native)
// - Should fall back to openai-native default model
restubStateManagerGet({
mode: "act",
actModeApiProvider: "openai-native",
actModeApiModelId: "claude-opus-4-5-20251101", // stale value - not in openAiNativeModels
})
const hookPath = path.join(tempDir, ".clinerules", "hooks", "PreToolUse")
const hookScript = `#!/usr/bin/env node
const input = JSON.parse(require('fs').readFileSync(0, 'utf-8'));
console.log(JSON.stringify({ cancel: false, contextModification: "Model ID: " + input.modelId }))`
await writeHookScript(hookPath, hookScript)
const factory = new HookFactory()
const runner = await factory.create("PreToolUse")
const result = await runner.run({
taskId: "test-task",
preToolUse: { toolName: "test_tool", parameters: {} },
})
result.cancel.should.be.false()
// Should use default openai-native model, not the stale claude model
result.contextModification!.should.equal("Model ID: openai-native:gpt-5.2")
})
it("should use valid openai-native model when apiModelId is correct", async () => {
restubStateManagerGet({
mode: "act",
actModeApiProvider: "openai-native",
actModeApiModelId: "gpt-5.2", // valid openai-native model
})
const hookPath = path.join(tempDir, ".clinerules", "hooks", "PreToolUse")
const hookScript = `#!/usr/bin/env node
const input = JSON.parse(require('fs').readFileSync(0, 'utf-8'));
console.log(JSON.stringify({ cancel: false, contextModification: "Model ID: " + input.modelId }))`
await writeHookScript(hookPath, hookScript)
const factory = new HookFactory()
const runner = await factory.create("PreToolUse")
const result = await runner.run({
taskId: "test-task",
preToolUse: { toolName: "test_tool", parameters: {} },
})
result.cancel.should.be.false()
result.contextModification!.should.equal("Model ID: openai-native:gpt-5.2")
})
it("should use openrouter-specific model ID field (not stale apiModelId)", async () => {
// Simulates switching to openrouter provider
// openrouter uses *ModeOpenRouterModelId, not *ModeApiModelId
restubStateManagerGet({
mode: "act",
actModeApiProvider: "openrouter",
actModeApiModelId: "gpt-5.2-codex", // stale value from prior provider
actModeOpenRouterModelId: "openai/gpt-5.2", // correct openrouter model
})
const hookPath = path.join(tempDir, ".clinerules", "hooks", "PreToolUse")
const hookScript = `#!/usr/bin/env node
const input = JSON.parse(require('fs').readFileSync(0, 'utf-8'));
console.log(JSON.stringify({ cancel: false, contextModification: "Model ID: " + input.modelId }))`
await writeHookScript(hookPath, hookScript)
const factory = new HookFactory()
const runner = await factory.create("PreToolUse")
const result = await runner.run({
taskId: "test-task",
preToolUse: { toolName: "test_tool", parameters: {} },
})
result.cancel.should.be.false()
// Should use openRouterModelId, not the stale apiModelId
result.contextModification!.should.equal("Model ID: openrouter:openai/gpt-5.2")
})
it("should use provider-specific model fields for other providers", async () => {
// Test ollama which uses *ModeOllamaModelId
restubStateManagerGet({
mode: "act",
actModeApiProvider: "ollama",
actModeApiModelId: "claude-opus-4-5-20251101", // stale
actModeOllamaModelId: "llama3:latest", // correct ollama model
})
const hookPath = path.join(tempDir, ".clinerules", "hooks", "PreToolUse")
const hookScript = `#!/usr/bin/env node
const input = JSON.parse(require('fs').readFileSync(0, 'utf-8'));
console.log(JSON.stringify({ cancel: false, contextModification: "Model ID: " + input.modelId }))`
await writeHookScript(hookPath, hookScript)
const factory = new HookFactory()
const runner = await factory.create("PreToolUse")
const result = await runner.run({
taskId: "test-task",
preToolUse: { toolName: "test_tool", parameters: {} },
})
result.cancel.should.be.false()
result.contextModification!.should.equal("Model ID: ollama:llama3:latest")
})
})
describe("Global Hooks", () => {
let globalHooksDir: string
let originalGetAllHooksDirs: any
@@ -39,6 +39,14 @@ describe("TaskCancel Hook", () => {
// Mock StateManager to return our temp directory
sandbox.stub(StateManager, "get").returns({
getGlobalStateKey: () => [{ path: tempDir }],
getGlobalSettingsKey: (key: string) => {
if (key === "mode") return "act"
if (key === "actModeApiProvider") return "anthropic"
if (key === "actModeApiModelId") return "claude-sonnet-4-20250514"
if (key === "planModeApiProvider") return "anthropic"
if (key === "planModeApiModelId") return "claude-sonnet-4-20250514"
return undefined
},
} as any)
getEnv = () => ({ tempDir })
@@ -39,6 +39,14 @@ describe("TaskComplete Hook", () => {
// Mock StateManager to return our temp directory
sandbox.stub(StateManager, "get").returns({
getGlobalStateKey: () => [{ path: tempDir }],
getGlobalSettingsKey: (key: string) => {
if (key === "mode") return "act"
if (key === "actModeApiProvider") return "anthropic"
if (key === "actModeApiModelId") return "claude-sonnet-4-20250514"
if (key === "planModeApiProvider") return "anthropic"
if (key === "planModeApiModelId") return "claude-sonnet-4-20250514"
return undefined
},
} as any)
getEnv = () => ({ tempDir })
@@ -37,6 +37,14 @@ describe("TaskResume Hook", () => {
// Mock StateManager to return our temp directory
sandbox.stub(StateManager, "get").returns({
getGlobalStateKey: () => [{ path: tempDir }],
getGlobalSettingsKey: (key: string) => {
if (key === "mode") return "act"
if (key === "actModeApiProvider") return "anthropic"
if (key === "actModeApiModelId") return "claude-sonnet-4-20250514"
if (key === "planModeApiProvider") return "anthropic"
if (key === "planModeApiModelId") return "claude-sonnet-4-20250514"
return undefined
},
} as any)
})
@@ -39,6 +39,14 @@ describe("TaskStart Hook", () => {
// Mock StateManager to return our temp directory
sandbox.stub(StateManager, "get").returns({
getGlobalStateKey: () => [{ path: tempDir }],
getGlobalSettingsKey: (key: string) => {
if (key === "mode") return "act"
if (key === "actModeApiProvider") return "anthropic"
if (key === "actModeApiModelId") return "claude-sonnet-4-20250514"
if (key === "planModeApiProvider") return "anthropic"
if (key === "planModeApiModelId") return "claude-sonnet-4-20250514"
return undefined
},
} as any)
getEnv = () => ({ tempDir })
@@ -37,6 +37,14 @@ describe("UserPromptSubmit Hook", () => {
// Mock StateManager to return our temp directory
sandbox.stub(StateManager, "get").returns({
getGlobalStateKey: () => [{ path: tempDir }],
getGlobalSettingsKey: (key: string) => {
if (key === "mode") return "act"
if (key === "actModeApiProvider") return "anthropic"
if (key === "actModeApiModelId") return "claude-sonnet-4-20250514"
if (key === "planModeApiProvider") return "anthropic"
if (key === "planModeApiModelId") return "claude-sonnet-4-20250514"
return undefined
},
} as any)
})
+97 -5
View File
@@ -1,5 +1,6 @@
import fs from "fs/promises"
import path from "path"
import { openAiCodexDefaultModelId, openAiCodexModels, openAiNativeDefaultModelId, openAiNativeModels } from "@/shared/api"
import { Logger } from "@/shared/services/Logger"
import { version as clineVersion } from "../../../package.json"
import { getDistinctId } from "../../services/logging/distinctId"
@@ -177,6 +178,7 @@ export abstract class HookRunner<Name extends HookName> {
* - timestamp: Execution time in milliseconds since epoch
* - workspaceRoots: Array of workspace folder paths
* - userId: Cline user ID, machine ID, or generated UUID
* - modelId: Currently selected provider and model in "provider:modelId" format
*
* This separation allows hook scripts to receive consistent metadata without
* requiring callers to manually provide it each time.
@@ -185,16 +187,106 @@ export abstract class HookRunner<Name extends HookName> {
* @returns Complete HookInput ready to be serialized and sent to the hook script
*/
protected async completeParams(params: NamedHookInput<Name>): Promise<HookInput> {
const workspaceRoots =
StateManager.get()
.getGlobalStateKey("workspaceRoots")
?.map((root) => root.path) || []
const stateManager = StateManager.get()
const workspaceRoots = stateManager.getGlobalStateKey("workspaceRoots")?.map((root) => root.path) || []
// Build modelId using the same format shown under the prompt input field in the VSCode extension
// (see webview-ui/src/components/chat/ChatTextArea.tsx)
const mode = stateManager.getGlobalSettingsKey("mode") || "act"
const apiProvider =
mode === "plan"
? stateManager.getGlobalSettingsKey("planModeApiProvider")
: stateManager.getGlobalSettingsKey("actModeApiProvider")
// Helper to get a mode-specific settings key
const getSetting = <T>(planKey: string, actKey: string): T | undefined =>
(mode === "plan"
? stateManager.getGlobalSettingsKey(planKey as any)
: stateManager.getGlobalSettingsKey(actKey as any)) as T | undefined
// Get the correct model ID based on provider
// This mirrors webview-ui/src/components/settings/utils/providerUtils.ts normalizeApiConfiguration()
// Each provider may have its own dedicated model ID field
const formattedModelId = (() => {
const apiModelId = getSetting<string>("planModeApiModelId", "actModeApiModelId")
switch (apiProvider) {
// Providers using openRouterModelId
case "cline":
return `cline:${getSetting<string>("planModeOpenRouterModelId", "actModeOpenRouterModelId") || ""}`
case "openrouter":
return `openrouter:${getSetting<string>("planModeOpenRouterModelId", "actModeOpenRouterModelId") || ""}`
// OpenAI compatible - uses openAiModelId
case "openai":
return `openai-compat:${getSetting<string>("planModeOpenAiModelId", "actModeOpenAiModelId") || ""}`
// OpenAI native - validate against known models
case "openai-native":
return `openai-native:${apiModelId && apiModelId in openAiNativeModels ? apiModelId : openAiNativeDefaultModelId}`
// OpenAI Codex - validate against known models
case "openai-codex":
return `openai-codex:${apiModelId && apiModelId in openAiCodexModels ? apiModelId : openAiCodexDefaultModelId}`
// Providers with their own dedicated model ID fields
case "requesty":
return `requesty:${getSetting<string>("planModeRequestyModelId", "actModeRequestyModelId") || ""}`
case "ollama":
return `ollama:${getSetting<string>("planModeOllamaModelId", "actModeOllamaModelId") || ""}`
case "lmstudio":
return `lmstudio:${getSetting<string>("planModeLmStudioModelId", "actModeLmStudioModelId") || ""}`
case "litellm":
return `litellm:${getSetting<string>("planModeLiteLlmModelId", "actModeLiteLlmModelId") || ""}`
case "groq":
return `groq:${getSetting<string>("planModeGroqModelId", "actModeGroqModelId") || ""}`
case "baseten":
return `baseten:${getSetting<string>("planModeBasetenModelId", "actModeBasetenModelId") || ""}`
case "huggingface":
return `huggingface:${getSetting<string>("planModeHuggingFaceModelId", "actModeHuggingFaceModelId") || ""}`
case "huawei-cloud-maas":
return `huawei-cloud-maas:${getSetting<string>("planModeHuaweiCloudMaasModelId", "actModeHuaweiCloudMaasModelId") || ""}`
case "vercel-ai-gateway":
return `vercel-ai-gateway:${getSetting<string>("planModeVercelAiGatewayModelId", "actModeVercelAiGatewayModelId") || ""}`
case "fireworks":
return `fireworks:${getSetting<string>("planModeFireworksModelId", "actModeFireworksModelId") || ""}`
case "oca":
return `oca:${getSetting<string>("planModeOcaModelId", "actModeOcaModelId") || ""}`
case "aihubmix":
return `aihubmix:${getSetting<string>("planModeAihubmixModelId", "actModeAihubmixModelId") || ""}`
case "nousResearch":
return `nousResearch:${getSetting<string>("planModeNousResearchModelId", "actModeNousResearchModelId") || ""}`
case "hicap":
return `hicap:${getSetting<string>("planModeHicapModelId", "actModeHicapModelId") || ""}`
case "together":
return `together:${getSetting<string>("planModeTogetherModelId", "actModeTogetherModelId") || ""}`
// VSCode LM has special format: vendor/family
case "vscode-lm": {
const selector = getSetting<{ vendor?: string; family?: string }>(
"planModeVsCodeLmModelSelector",
"actModeVsCodeLmModelSelector",
)
return `vscode-lm:${selector ? `${selector.vendor || ""}/${selector.family || ""}` : ""}`
}
// Dify has hardcoded model
case "dify":
return "dify:dify-workflow"
// All other providers use apiModelId (anthropic, bedrock, vertex, gemini, deepseek, etc.)
default:
return `${apiProvider || ""}:${apiModelId || ""}`
}
})()
return {
clineVersion,
hookName: this.hookName,
timestamp: Date.now().toString(),
workspaceRoots,
userId: getDistinctId(), // Always available: Cline User ID, machine ID, or generated UUID
modelId: formattedModelId,
...params,
}
}
@@ -353,7 +445,7 @@ class StdioHookRunner<Name extends HookName> extends HookRunner<Name> {
}
return output
} catch (parseError) {
} catch (_parseError) {
// Try to extract JSON from stdout (it might have debug output before/after)
// Scan from the end to find the last complete JSON object
// This handles cases where hooks output debug info before the actual JSON response
@@ -47,7 +47,9 @@ describe("TerminalProcess (Integration Tests)", () => {
// Remove any event listeners left on the TerminalProcess
process.removeAllListeners()
// Dispose all terminals created during the test
createdTerminals.forEach((t) => t.dispose())
createdTerminals.forEach((t) => {
t.dispose()
})
createdTerminals = []
})
@@ -218,9 +220,7 @@ describe("TerminalProcess (Integration Tests)", () => {
// Check that the correct methods were called and events emitted
sendTextStub.calledWith("test-command", true).should.be.true()
;(emitSpy as sinon.SinonSpy).calledWith("completed").should.be.true()
;(emitSpy as sinon.SinonSpy)
.calledWith("continue")
.should.be.true()
;(emitSpy as sinon.SinonSpy).calledWith("continue").should.be.true()
// This event should be emitted for terminals without shell integration
;(emitSpy as sinon.SinonSpy).calledWith("no_shell_integration").should.be.true()
@@ -254,9 +254,7 @@ describe("TerminalProcess (Integration Tests)", () => {
await process.run(terminal, "echo test")
// Verify the executeCommand was called with the right command
mockExecuteCommand
.calledWith("echo test")
.should.be.true()
mockExecuteCommand.calledWith("echo test").should.be.true()
// Check that the events were emitted
;(emitSpy as sinon.SinonSpy).calledWith("completed").should.be.true()
@@ -375,9 +373,7 @@ describe("TerminalProcess (Integration Tests)", () => {
// Check that "test-command" was filtered out but "test command" was not
;(emitSpy as sinon.SinonSpy).calledWith("line", "test command").should.be.true()
;(emitSpy as sinon.SinonSpy)
.calledWith("line", "other output")
.should.be.true()
;(emitSpy as sinon.SinonSpy).calledWith("line", "other output").should.be.true()
// This should never be called because it should be filtered
;(emitSpy as sinon.SinonSpy).calledWith("line", "test-command").should.be.false()
})