diff --git a/src/bin-launcher.test.ts b/src/bin-launcher.test.ts index 00b43a99f..f1f5fba37 100644 --- a/src/bin-launcher.test.ts +++ b/src/bin-launcher.test.ts @@ -4,7 +4,7 @@ import { cpSync, mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } fr import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; -import { writeTestDevspaceConfig } from "./test-support/config.test.js"; +import { writeTestDevspaceConfig } from "./test-support/config.js"; const projectRoot = fileURLToPath(new URL("..", import.meta.url)); const tsxRoot = join(projectRoot, "node_modules", "tsx"); diff --git a/src/cli-show-changes.test.ts b/src/cli-show-changes.test.ts index 40763b86e..b147b7605 100644 --- a/src/cli-show-changes.test.ts +++ b/src/cli-show-changes.test.ts @@ -9,7 +9,7 @@ import test from "node:test"; import { fileURLToPath } from "node:url"; import { promisify } from "node:util"; import { createReviewCheckpointManager } from "./review-checkpoints.js"; -import { writeTestDevspaceConfig } from "./test-support/config.test.js"; +import { writeTestDevspaceConfig } from "./test-support/config.js"; const execFileAsync = promisify(execFile); const require = createRequire(import.meta.url); diff --git a/src/cli-worktrees.test.ts b/src/cli-worktrees.test.ts index f749e2afa..2d6a40a3d 100644 --- a/src/cli-worktrees.test.ts +++ b/src/cli-worktrees.test.ts @@ -7,7 +7,7 @@ import { fileURLToPath } from "node:url"; import test from "node:test"; import { promisify } from "node:util"; import { openDatabase } from "./db/client.js"; -import { writeTestDevspaceConfig } from "./test-support/config.test.js"; +import { writeTestDevspaceConfig } from "./test-support/config.js"; import { SqliteWorkspaceStore } from "./workspace-store.js"; const execFileAsync = promisify(execFile); diff --git a/src/cli.test.ts b/src/cli.test.ts index 3a622eb19..a70f4d5d2 100644 --- a/src/cli.test.ts +++ b/src/cli.test.ts @@ -14,7 +14,7 @@ import { } from "./local-agent-daemon-lifecycle.js"; import { encodeLocalAgentDaemonResponse } from "./local-agent-daemon-protocol.js"; import { LocalAgentStore } from "./local-agent-store.js"; -import { writeTestDevspaceConfig } from "./test-support/config.test.js"; +import { writeTestDevspaceConfig } from "./test-support/config.js"; const execFileAsync = promisify(execFile); const require = createRequire(import.meta.url); diff --git a/src/local-agent-catalog.test.ts b/src/local-agent-catalog.test.ts deleted file mode 100644 index cc156251d..000000000 --- a/src/local-agent-catalog.test.ts +++ /dev/null @@ -1,60 +0,0 @@ -import assert from "node:assert/strict"; -import { - buildLocalAgentCatalog, - buildLocalAgentProviderStatuses, -} from "./local-agent-catalog.js"; -import type { LocalAgentProfile } from "./local-agent-profiles.js"; -import type { SubagentsConfig } from "./local-agent-config.js"; - -const config: SubagentsConfig = { - enabled: true, - instructions: "on-demand", - providers: [ - { id: "codex", enabled: true, model: "gpt-default", effort: "medium" }, - { id: "claude", enabled: true, model: "sonnet" }, - { id: "pi", enabled: false }, - ], -}; -const statuses = buildLocalAgentProviderStatuses(config, [ - { name: "codex", available: true }, - { name: "claude", available: false, reason: "credentials missing" }, - { name: "pi", available: true }, -]); -assert.equal(statuses.find((provider) => provider.id === "codex")?.usable, true); -assert.equal(statuses.find((provider) => provider.id === "claude")?.usable, false); -assert.equal(statuses.find((provider) => provider.id === "pi")?.usable, false); -assert.equal(statuses.find((provider) => provider.id === "opencode")?.enabled, false); - -const profiles: LocalAgentProfile[] = [ - { - name: "reviewer", - description: "Review changes.", - provider: "codex", - filePath: "/project/reviewer.md", - body: "Review only.", - disabled: false, - }, - { - name: "custom", - description: "Use a custom model.", - provider: "codex", - model: "gpt-custom", - filePath: "/project/custom.md", - body: "Inspect.", - disabled: false, - }, - { - name: "claude-reviewer", - description: "Unavailable profile.", - provider: "claude", - filePath: "/project/claude.md", - body: "Review.", - disabled: false, - }, -]; -const catalog = buildLocalAgentCatalog(config, profiles, statuses); -assert.deepEqual(catalog.providers.map((provider) => provider.id), ["codex", "claude"]); -assert.deepEqual(catalog.profiles.map((profile) => profile.name), ["reviewer", "custom"]); -assert.equal(catalog.profiles[0]?.model, "gpt-default"); -assert.equal(catalog.profiles[0]?.effort, "medium"); -assert.equal(catalog.profiles[1]?.model, "gpt-custom"); diff --git a/src/local-agent-presentation.test.ts b/src/local-agent-presentation.test.ts index ea1a4d6e6..afaeaedcc 100644 --- a/src/local-agent-presentation.test.ts +++ b/src/local-agent-presentation.test.ts @@ -3,56 +3,34 @@ import type { LocalAgentCatalog } from "./local-agent-catalog.js"; import { presentAgentObservation, presentAgentReceipt, - presentAgentSummary, presentAgentTargetCatalog, } from "./local-agent-presentation.js"; import type { LocalAgentRecord } from "./local-agent-store.js"; -const record: LocalAgentRecord = { +const starting: LocalAgentRecord = { id: "agt_test", - workspaceId: "ws_private", workspaceRoot: "/private/project", profileName: "reviewer", provider: "codex", - model: "gpt-5.4", - effort: "high", - providerSessionId: "provider_private", - status: "running", - latestResponse: "previous response", + status: "starting", createdAt: "2026-08-21T10:00:00.000Z", updatedAt: "2026-08-21T10:01:00.000Z", }; -assert.deepEqual(presentAgentReceipt({ ...record, status: "starting" }), { +assert.deepEqual(presentAgentReceipt(starting), { id: "agt_test", status: "running", }); -assert.deepEqual(presentAgentSummary({ ...record, status: "idle" }), { - id: "agt_test", - status: "completed", - target: "reviewer", -}); - -const completed = presentAgentObservation({ - ...record, - status: "idle", - latestResponse: "Found one issue.", -}); -assert.deepEqual(completed, { - id: "agt_test", - status: "completed", - response: "Found one issue.", -}); -const failed = presentAgentObservation({ - ...record, +const failed: LocalAgentRecord = { + ...starting, status: "error", - latestResponse: undefined, error: "Provider disconnected.", errorCode: "PROVIDER_EXECUTION_ERROR", errorRetryable: true, -}); -assert.deepEqual(failed, { +}; + +assert.deepEqual(presentAgentObservation(failed), { id: "agt_test", status: "failed", error: { @@ -66,13 +44,7 @@ const catalog: LocalAgentCatalog = { enabled: true, providers: [ { id: "codex", enabled: true, available: true, usable: true, model: "gpt-5.4", effort: "high" }, - { - id: "claude", - enabled: true, - available: false, - usable: false, - reason: "credentials missing", - }, + { id: "claude", enabled: true, available: false, usable: false }, ], profiles: [{ name: "reviewer", @@ -82,8 +54,8 @@ const catalog: LocalAgentCatalog = { effort: "high", }], }; -const targetCatalog = presentAgentTargetCatalog(catalog); -assert.deepEqual(targetCatalog, { + +assert.deepEqual(presentAgentTargetCatalog(catalog), { targets: [ { name: "codex", kind: "provider", model: "gpt-5.4", effort: "high" }, { diff --git a/src/local-agent-profiles.test.ts b/src/local-agent-profiles.test.ts index 8a7d57db1..7751e2fc5 100644 --- a/src/local-agent-profiles.test.ts +++ b/src/local-agent-profiles.test.ts @@ -4,7 +4,7 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { loadConfig } from "./config.js"; import { loadLocalAgentProfiles } from "./local-agent-profiles.js"; -import { writeTestDevspaceConfig } from "./test-support/config.test.js"; +import { writeTestDevspaceConfig } from "./test-support/config.js"; const root = await mkdtemp(join(tmpdir(), "devspace-agent-profiles-test-")); diff --git a/src/request-meta.test.ts b/src/request-meta.test.ts deleted file mode 100644 index f00ed5f95..000000000 --- a/src/request-meta.test.ts +++ /dev/null @@ -1,24 +0,0 @@ -import assert from "node:assert/strict"; -import test from "node:test"; -import { conversationScopeIdFromRequestMeta } from "./request-meta.js"; - -test("conversation scope resolves valid OpenAI session metadata", () => { - for (const meta of [ - undefined, - {}, - { "openai/session": "" }, - { "openai/session": 42 }, - { "openai/session": {} }, - ]) { - assert.equal(conversationScopeIdFromRequestMeta(meta), undefined); - } - - assert.equal( - conversationScopeIdFromRequestMeta({ - "openai/session": "chat-session-opaque-value", - "openai/subject": "user-1", - "openai/organization": "org-1", - }), - "chat-session-opaque-value", - ); -}); diff --git a/src/server-oauth.test.ts b/src/server-oauth.test.ts index 1ab4fcfb7..f9cdf5720 100644 --- a/src/server-oauth.test.ts +++ b/src/server-oauth.test.ts @@ -8,7 +8,7 @@ import test from "node:test"; import { loadConfig } from "./config.js"; import { SqliteOAuthStore } from "./oauth-store.js"; import { createServer } from "./server.js"; -import { writeTestDevspaceConfig } from "./test-support/config.test.js"; +import { writeTestDevspaceConfig } from "./test-support/config.js"; test("HTTP MCP enforces canonical and exact alias bearer resources", async (t) => { const root = await mkdtemp(join(tmpdir(), "devspace-http-oauth-")); diff --git a/src/server.test.ts b/src/server.test.ts index 59bcc5d37..888ebc30d 100644 --- a/src/server.test.ts +++ b/src/server.test.ts @@ -17,7 +17,7 @@ import { ProcessSessionManager } from "./process-sessions.js"; import { createMcpServer, createServer } from "./server.js"; import { SqliteWorkspaceStore } from "./workspace-store.js"; import { WorkspaceRegistry } from "./workspaces.js"; -import { writeTestDevspaceConfig } from "./test-support/config.test.js"; +import { writeTestDevspaceConfig } from "./test-support/config.js"; const execFileAsync = promisify(execFile); @@ -386,8 +386,36 @@ test("open_workspace keeps lifecycle flags out of model output and preserves com test("open_workspace refreshes provider availability for each catalog", async (t) => { let available = false; const context = await fixture(t, { - localAgentProviders: () => [{ name: "codex", available }], + localAgentProviders: () => [ + { name: "codex", available }, + { name: "claude", available: false }, + ], + subagents: { + enabled: true, + instructions: "on-demand", + providers: [ + { id: "codex", enabled: true, model: "gpt-default", effort: "medium" }, + { id: "claude", enabled: true }, + ], + }, }); + await writeFile(join(context.project, ".devspace", "agents", "custom.md"), [ + "---", + "name: custom", + "description: Uses a custom model.", + "provider: codex", + "model: gpt-custom", + "---", + "Inspect.", + ].join("\n")); + await writeFile(join(context.project, ".devspace", "agents", "claude-reviewer.md"), [ + "---", + "name: claude-reviewer", + "description: Uses an unavailable provider.", + "provider: claude", + "---", + "Review with Claude.", + ].join("\n")); const unavailable = structuredContent(await callOpen(context.client, context.project, "chat-1")); assert.deepEqual(unavailable.agent_providers, []); @@ -395,14 +423,31 @@ test("open_workspace refreshes provider availability for each catalog", async (t available = true; const usable = structuredContent(await callOpen(context.client, context.project, "chat-2")); - assert.equal( - (usable.agent_providers as Array>)[0]?.id, - "codex", + assert.deepEqual( + (usable.agent_providers as Array>).map((provider) => provider.id), + ["codex"], ); - assert.equal( - (usable.agents as Array>)[0]?.name, - "reviewer", + const provider = (usable.agent_providers as Array>)[0]; + assert.deepEqual( + { id: provider?.id, model: provider?.model, effort: provider?.effort }, + { id: "codex", model: "gpt-default", effort: "medium" }, ); + const agents = usable.agents as Array>; + assert.deepEqual(agents.find((agent) => agent.name === "reviewer"), { + name: "reviewer", + description: "Reviews project changes.", + provider: "codex", + model: "gpt-default", + effort: "medium", + }); + assert.deepEqual(agents.find((agent) => agent.name === "custom"), { + name: "custom", + description: "Uses a custom model.", + provider: "codex", + model: "gpt-custom", + effort: "medium", + }); + assert.deepEqual(agents.map((agent) => agent.name).sort(), ["custom", "reviewer"]); }); test("open_workspace omits providers disabled by configuration", async (t) => { @@ -468,6 +513,17 @@ test("open_workspace scopes checkout reuse to OpenAI session metadata", async (t assert.notEqual(structuredContent(unscoped).workspace_id, structuredContent(first).workspace_id); assert.ok(Array.isArray(structuredContent(otherSession).agents_files)); assert.ok(Array.isArray(structuredContent(unscoped).agents_files)); + + for (const malformedSession of ["", 42, {}]) { + const firstMalformed = await callOpen(context.client, context.project, malformedSession); + const repeatedMalformed = await callOpen(context.client, context.project, malformedSession); + assert.notEqual( + structuredContent(repeatedMalformed).workspace_id, + structuredContent(firstMalformed).workspace_id, + ); + assert.ok(Array.isArray(structuredContent(firstMalformed).agents_files)); + assert.ok(Array.isArray(structuredContent(repeatedMalformed).agents_files)); + } }); test("HTTP endpoint serves modern MCP and stateless legacy clients", async (t) => { @@ -953,12 +1009,12 @@ function recordValue(value: unknown): Record { async function callOpen( client: Client, path: string, - conversationScopeId?: string, + conversationScopeId?: unknown, ): Promise>> { const params = { name: "open_workspace", arguments: { path }, - ...(conversationScopeId + ...(conversationScopeId !== undefined ? { _meta: { "openai/session": conversationScopeId } } : {}), } as Parameters[0]; diff --git a/src/skills.test.ts b/src/skills.test.ts index 357c417db..e3e57371e 100644 --- a/src/skills.test.ts +++ b/src/skills.test.ts @@ -9,7 +9,7 @@ import { loadWorkspaceSkills, resolveSkillReadPath, } from "./skills.js"; -import { writeTestDevspaceConfig } from "./test-support/config.test.js"; +import { writeTestDevspaceConfig } from "./test-support/config.js"; const root = await mkdtemp(join(tmpdir(), "devspace-skills-test-")); const originalHome = process.env.HOME; diff --git a/src/test-support/config.test.ts b/src/test-support/config.ts similarity index 100% rename from src/test-support/config.test.ts rename to src/test-support/config.ts diff --git a/src/workspace-conversation.test.ts b/src/workspace-conversation.test.ts index 67ebbe2ac..d7d3e2979 100644 --- a/src/workspace-conversation.test.ts +++ b/src/workspace-conversation.test.ts @@ -9,7 +9,7 @@ import { loadConfig, type ServerConfig } from "./config.js"; import { openDatabase } from "./db/client.js"; import { SqliteWorkspaceStore } from "./workspace-store.js"; import { WorkspaceRegistry } from "./workspaces.js"; -import { writeTestDevspaceConfig } from "./test-support/config.test.js"; +import { writeTestDevspaceConfig } from "./test-support/config.js"; const execFileAsync = promisify(execFile); diff --git a/src/workspaces.test.ts b/src/workspaces.test.ts index 914837502..c79dd4584 100644 --- a/src/workspaces.test.ts +++ b/src/workspaces.test.ts @@ -13,7 +13,7 @@ import { type WorkspaceStoreError, } from "./workspace-store.js"; import { WorkspaceRegistry } from "./workspaces.js"; -import { writeTestDevspaceConfig } from "./test-support/config.test.js"; +import { writeTestDevspaceConfig } from "./test-support/config.js"; const execFileAsync = promisify(execFile); diff --git a/test/package-install-smoke.test.ts b/test/package-install-smoke.test.ts index c53477fd8..4400628ed 100644 --- a/test/package-install-smoke.test.ts +++ b/test/package-install-smoke.test.ts @@ -4,7 +4,7 @@ import { mkdirSync, mkdtempSync, readdirSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; -import { writeTestDevspaceConfig } from "../src/test-support/config.test.js"; +import { writeTestDevspaceConfig } from "../src/test-support/config.js"; const projectRoot = fileURLToPath(new URL("..", import.meta.url)); diff --git a/tsconfig.build.json b/tsconfig.build.json index a48941b6a..94435e351 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -1,4 +1,4 @@ { "extends": "./tsconfig.json", - "exclude": ["src/ui/**/*", "src/**/*.test.ts"] + "exclude": ["src/ui/**/*", "src/test-support/**/*", "src/**/*.test.ts"] }