Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 68 additions & 2 deletions assistant/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8600,6 +8600,16 @@ paths:
- type
- credential
additionalProperties: false
status:
type: string
enum:
- active
- disabled
label:
anyOf:
- type: string
minLength: 1
- type: "null"
createdAt:
type: integer
minimum: -9007199254740991
Expand All @@ -8612,6 +8622,8 @@ paths:
- name
- provider
- auth
- status
- label
- createdAt
- updatedAt
additionalProperties: false
Expand Down Expand Up @@ -8705,6 +8717,16 @@ paths:
- type
- credential
additionalProperties: false
status:
type: string
enum:
- active
- disabled
label:
anyOf:
- type: string
minLength: 1
- type: "null"
createdAt:
type: integer
minimum: -9007199254740991
Expand All @@ -8717,6 +8739,8 @@ paths:
- name
- provider
- auth
- status
- label
- createdAt
- updatedAt
additionalProperties: false
Expand Down Expand Up @@ -8797,6 +8821,14 @@ paths:
- type
- credential
additionalProperties: false
label:
type: string
minLength: 1
status:
type: string
enum:
- active
- disabled
required:
- name
- provider
Expand Down Expand Up @@ -8913,6 +8945,16 @@ paths:
- type
- credential
additionalProperties: false
status:
type: string
enum:
- active
- disabled
label:
anyOf:
- type: string
minLength: 1
- type: "null"
createdAt:
type: integer
minimum: -9007199254740991
Expand All @@ -8925,6 +8967,8 @@ paths:
- name
- provider
- auth
- status
- label
- createdAt
- updatedAt
additionalProperties: false
Expand All @@ -8938,8 +8982,8 @@ paths:
type: string
patch:
operationId: inference_providerconnections_by_name_patch
summary: Update a provider connection's auth
description: Update the auth configuration for an existing connection. Cannot rename or change the provider.
summary: Update a provider connection
description: Update an existing connection. Cannot rename or change the provider.
tags:
- inference
responses:
Expand Down Expand Up @@ -9016,6 +9060,16 @@ paths:
- type
- credential
additionalProperties: false
status:
type: string
enum:
- active
- disabled
label:
anyOf:
- type: string
minLength: 1
- type: "null"
createdAt:
type: integer
minimum: -9007199254740991
Expand All @@ -9028,6 +9082,8 @@ paths:
- name
- provider
- auth
- status
- label
- createdAt
- updatedAt
additionalProperties: false
Expand Down Expand Up @@ -9102,6 +9158,16 @@ paths:
- type
- credential
additionalProperties: false
status:
type: string
enum:
- active
- disabled
label:
anyOf:
- type: string
minLength: 1
- type: "null"
required:
- auth
additionalProperties: false
Expand Down
43 changes: 43 additions & 0 deletions assistant/src/__tests__/profile-entry-status.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { describe, expect, test } from "bun:test";

import { ProfileEntry } from "../config/schemas/llm.js";

describe("ProfileEntry status field", () => {
test("parses a profile with status: disabled", () => {
const result = ProfileEntry.safeParse({
status: "disabled",
});
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.status).toBe("disabled");
}
});

test("parses a profile without status — status is undefined (treated as active)", () => {
const result = ProfileEntry.safeParse({
provider: "anthropic",
model: "claude-opus-4-7",
});
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.status).toBeUndefined();
}
});

test("parses a profile with status: active", () => {
const result = ProfileEntry.safeParse({
status: "active",
});
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.status).toBe("active");
}
});

test("rejects an invalid status value", () => {
const result = ProfileEntry.safeParse({
status: "hidden",
});
expect(result.success).toBe(false);
});
});
5 changes: 5 additions & 0 deletions assistant/src/config/schemas/llm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,9 @@ const LLMConfigFragment = z.object({
});
type LLMConfigFragment = z.infer<typeof LLMConfigFragment>;

export const ProfileStatusSchema = z.enum(["active", "disabled"]);
export type ProfileStatus = z.infer<typeof ProfileStatusSchema>;

/**
* A named profile entry: an `LLMConfigFragment` augmented with
* presentation/ownership metadata. These fields are intentionally kept off
Expand All @@ -343,6 +346,8 @@ export const ProfileEntry = LLMConfigFragment.extend({
* not yet backfilled by the boot-time migration.
*/
provider_connection: z.string().min(1).optional(),
/** Absent means active. */
status: ProfileStatusSchema.optional(),
});
export type ProfileEntry = z.infer<typeof ProfileEntry>;

Expand Down
2 changes: 2 additions & 0 deletions assistant/src/memory/db-init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ import {
migrateOAuthProvidersScopeSeparator,
migrateOAuthProvidersTokenAuthMethodDefault,
migrateOAuthProvidersTokenExchangeBodyFormat,
migrateProviderConnectionStatusLabel,
migrateReminderRoutingIntent,
migrateRemindersToSchedules,
migrateRenameConversationTypeColumn,
Expand Down Expand Up @@ -418,6 +419,7 @@ export function initializeDb(): void {
migrateConversationInferenceProfileSession,
migrateMessageBookmarks,
migrateCreateProviderConnections,
migrateProviderConnectionStatusLabel,
];

// Run each migration step, catching and logging individual failures so one
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { type DrizzleDb, getSqliteFrom } from "../db-connection.js";

/**
* Adds `status` (NOT NULL DEFAULT 'active') and `label` (nullable) columns
* to the `provider_connections` table.
*
* Idempotent: reads PRAGMA table_info before each ALTER so re-running on a
* database that already has the columns is a no-op.
*/
export function migrateProviderConnectionStatusLabel(database: DrizzleDb): void {
const raw = getSqliteFrom(database);

const columns = raw.query(`PRAGMA table_info(provider_connections)`).all() as Array<{ name: string }>;
const columnNames = new Set(columns.map((c) => c.name));

if (!columnNames.has("status")) {
raw.exec(`ALTER TABLE provider_connections ADD COLUMN status TEXT NOT NULL DEFAULT 'active'`);
}

if (!columnNames.has("label")) {
raw.exec(`ALTER TABLE provider_connections ADD COLUMN label TEXT`);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { Database } from "bun:sqlite";
import { describe, expect, test } from "bun:test";

import { drizzle } from "drizzle-orm/bun-sqlite";

import { getSqliteFrom } from "../../db-connection.js";
import * as schema from "../../schema.js";
import { migrateCreateProviderConnections } from "../243-provider-connections.js";
import { migrateProviderConnectionStatusLabel } from "../244-provider-connection-status-label.js";

interface ColumnRow {
name: string;
type: string;
notnull: number;
dflt_value: string | null;
pk: number;
}

function createTestDb() {
const sqlite = new Database(":memory:");
sqlite.exec("PRAGMA journal_mode=WAL");
sqlite.exec("PRAGMA foreign_keys = ON");
return drizzle(sqlite, { schema });
}

describe("migration 244 — provider_connection status + label", () => {
test("adds status (NOT NULL DEFAULT active) and label (nullable) columns", () => {
const db = createTestDb();
const raw = getSqliteFrom(db);

// Bootstrap provider_connections table via migration 243.
migrateCreateProviderConnections(db);

// Verify the new columns are absent before migration 244.
const colsBefore = raw.query(`PRAGMA table_info(provider_connections)`).all() as ColumnRow[];
const namesBefore = colsBefore.map((c) => c.name);
expect(namesBefore).not.toContain("status");
expect(namesBefore).not.toContain("label");

migrateProviderConnectionStatusLabel(db);

const cols = raw.query(`PRAGMA table_info(provider_connections)`).all() as ColumnRow[];
const colMap = Object.fromEntries(cols.map((c) => [c.name, c]));

// status: NOT NULL, default 'active'
expect(colMap["status"]).toBeDefined();
expect(colMap["status"].notnull).toBe(1);
expect(colMap["status"].dflt_value).toBe("'active'");

// label: nullable
expect(colMap["label"]).toBeDefined();
expect(colMap["label"].notnull).toBe(0);
expect(colMap["label"].dflt_value).toBeNull();
});

test("is idempotent — running twice does not throw", () => {
const db = createTestDb();
migrateCreateProviderConnections(db);
migrateProviderConnectionStatusLabel(db);
expect(() => migrateProviderConnectionStatusLabel(db)).not.toThrow();
});

test("existing rows get status=active from column default", () => {
const db = createTestDb();
const raw = getSqliteFrom(db);

// Seed a row before adding the status column.
migrateCreateProviderConnections(db);

// Verify canonical connections got the default.
migrateProviderConnectionStatusLabel(db);

const rows = raw.query(`SELECT name, status, label FROM provider_connections`).all() as Array<{
name: string;
status: string;
label: string | null;
}>;
expect(rows.length).toBeGreaterThan(0);
for (const row of rows) {
expect(row.status).toBe("active");
expect(row.label).toBeNull();
}
});
});
1 change: 1 addition & 0 deletions assistant/src/memory/migrations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ export { migrateConversationInferenceProfileSession } from "./240-conversation-i
export { migrateActivationStateFkCascade } from "./241-activation-state-fk-cascade.js";
export { migrateMessageBookmarks } from "./242-message-bookmarks.js";
export { migrateCreateProviderConnections } from "./243-provider-connections.js";
export { migrateProviderConnectionStatusLabel } from "./244-provider-connection-status-label.js";
export {
MIGRATION_REGISTRY,
type MigrationRegistryEntry,
Expand Down
2 changes: 2 additions & 0 deletions assistant/src/memory/schema/inference.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ export const providerConnections = sqliteTable(
name: text("name").primaryKey(),
provider: text("provider").notNull(),
auth: text("auth").notNull(),
status: text("status").notNull().default("active"),
label: text("label"),
createdAt: integer("created_at").notNull(),
updatedAt: integer("updated_at").notNull(),
},
Expand Down
2 changes: 2 additions & 0 deletions assistant/src/providers/__tests__/inference.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { drizzle } from "drizzle-orm/bun-sqlite";
import type { DrizzleDb } from "../../memory/db-connection.js";
import { getSqliteFrom } from "../../memory/db-connection.js";
import { migrateCreateProviderConnections } from "../../memory/migrations/243-provider-connections.js";
import { migrateProviderConnectionStatusLabel } from "../../memory/migrations/244-provider-connection-status-label.js";
import * as schema from "../../memory/schema.js";
import { AuthSchema } from "../inference/auth.js";
import {
Expand All @@ -33,6 +34,7 @@ function setupDb(): { db: DrizzleDb; raw: Database } {
const db = drizzle(sqlite, { schema });
const raw = getSqliteFrom(db);
migrateCreateProviderConnections(db);
migrateProviderConnectionStatusLabel(db);
return { db, raw };
}

Expand Down
Loading
Loading