Scope provider requests to workspaces
This commit is contained in:
1 parent
ee5eff7725
commit
335d57b77f
15 files changed
+144
-16
No files matched your search
@@ -0,0 +1,10 @@
|
||||
export const providerRequestWorkspaceSchema = `
|
||||
alter table provider_requests add column workspace_id text references workspaces(id) on delete set null;
|
||||
|
||||
create index if not exists idx_provider_requests_workspace
|
||||
on provider_requests(org_id, workspace_id, created_at)
|
||||
where workspace_id is not null;
|
||||
|
||||
insert into schema_migrations(version, name, applied_at)
|
||||
values (31, 'provider_request_workspace', datetime('now'));
|
||||
`;
|
||||
@@ -29,6 +29,7 @@ import { socialAuditRecordsSchema } from "./migrations/027-social-audit-records"
|
||||
import { migrationCutoverJournalSchema } from "./migrations/028-migration-cutover-journal";
|
||||
import { fileHandoffRecordsSchema } from "./migrations/029-file-handoff-records";
|
||||
import { approvalSourceRefsSchema } from "./migrations/030-approval-source-refs";
|
||||
import { providerRequestWorkspaceSchema } from "./migrations/031-provider-request-workspace";
|
||||
|
||||
interface Migration {
|
||||
version: number;
|
||||
@@ -67,6 +68,7 @@ const MIGRATIONS: Migration[] = [
|
||||
{ version: 28, name: "migration_cutover_journal", sql: migrationCutoverJournalSchema },
|
||||
{ version: 29, name: "file_handoff_records", sql: fileHandoffRecordsSchema },
|
||||
{ version: 30, name: "approval_source_refs", sql: approvalSourceRefsSchema },
|
||||
{ version: 31, name: "provider_request_workspace", sql: providerRequestWorkspaceSchema },
|
||||
];
|
||||
|
||||
const SCHEMA_VERSION = MIGRATIONS[MIGRATIONS.length - 1].version;
|
||||
|
||||
@@ -194,6 +194,7 @@ const RELATION_CHECKS: RelationCheck[] = [
|
||||
{ table: "providers", column: "credential_id", referencedTable: "credentials", referencedColumn: "id", reason: "dangling_credential_id" },
|
||||
{ table: "provider_requests", column: "org_id", referencedTable: "organizations", referencedColumn: "id", reason: "dangling_org_id" },
|
||||
{ table: "provider_requests", column: "provider_id", referencedTable: "providers", referencedColumn: "id", reason: "dangling_provider_id" },
|
||||
{ table: "provider_requests", column: "workspace_id", referencedTable: "workspaces", referencedColumn: "id", reason: "dangling_workspace_id" },
|
||||
{ table: "provider_usage_windows", column: "org_id", referencedTable: "organizations", referencedColumn: "id", reason: "dangling_org_id" },
|
||||
{ table: "provider_usage_windows", column: "provider_id", referencedTable: "providers", referencedColumn: "id", reason: "dangling_provider_id" },
|
||||
{ table: "provider_rate_limit_observations", column: "org_id", referencedTable: "organizations", referencedColumn: "id", reason: "dangling_org_id" },
|
||||
|
||||
@@ -57,6 +57,7 @@ export class ProviderBackgroundService {
|
||||
orgId: input.orgId,
|
||||
provider: execution.provider,
|
||||
model: execution.provider.model,
|
||||
workspaceId: execution.workspaceId,
|
||||
promptSha256: hashPrompt(execution.prompt),
|
||||
inputCharCount: execution.prompt.length,
|
||||
executionMode: PROVIDER_EXECUTION_MODE.BACKGROUND,
|
||||
@@ -201,12 +202,14 @@ export class ProviderBackgroundService {
|
||||
const prompt = input.prompt.trim();
|
||||
if (!prompt) throw new Error("Prompt is required.");
|
||||
const provider = this.loadConfiguredProvider(input.orgId, input.providerId);
|
||||
this.enforcePolicy(input.orgId, provider, input.executionMode ?? PROVIDER_EXECUTION_MODE.BACKGROUND);
|
||||
const workspaceId = this.normalizeWorkspaceId(input.orgId, input.workspaceId ?? null);
|
||||
this.enforcePolicy(input.orgId, provider, input.executionMode ?? PROVIDER_EXECUTION_MODE.BACKGROUND, workspaceId);
|
||||
if (provider.auth_type !== PROVIDER_AUTH_TYPE.API_KEY) {
|
||||
throw new Error("Provider background execution requires an API key adapter.");
|
||||
}
|
||||
if (!provider.model) throw new Error("Provider model is required.");
|
||||
return {
|
||||
workspaceId,
|
||||
prompt,
|
||||
instructions: input.instructions?.trim() || null,
|
||||
provider: { ...provider, model: provider.model },
|
||||
@@ -215,8 +218,12 @@ export class ProviderBackgroundService {
|
||||
};
|
||||
}
|
||||
|
||||
private enforcePolicy(orgId: string, provider: ProviderRow, executionMode: ProviderExecutionMode): void {
|
||||
private enforcePolicy(orgId: string, provider: ProviderRow, executionMode: ProviderExecutionMode, workspaceId: string | null): void {
|
||||
if (executionMode !== PROVIDER_EXECUTION_MODE.BACKGROUND) throw new Error("Provider background execution mode is required.");
|
||||
if (provider.workspace_scope !== "organization" && provider.workspace_scope !== "workspace") throw new Error("Provider workspace scope is invalid.");
|
||||
if (provider.workspace_scope === "workspace" && !workspaceId) {
|
||||
throw new Error("Provider workspace scope requires a workspace context.");
|
||||
}
|
||||
if (provider.data_policy === PROVIDER_DATA_POLICY.ZERO_DATA_RETENTION) {
|
||||
throw new Error("Background execution conflicts with zero data retention policy.");
|
||||
}
|
||||
@@ -234,6 +241,15 @@ export class ProviderBackgroundService {
|
||||
if (requestCount >= provider.monthly_request_limit) throw new Error("Provider monthly request limit has been reached.");
|
||||
}
|
||||
|
||||
private normalizeWorkspaceId(orgId: string, workspaceId: string | null): string | null {
|
||||
const normalized = workspaceId?.trim() || null;
|
||||
if (!normalized) return null;
|
||||
const workspace = this.db.prepare("select id from workspaces where id = ? and org_id = ?")
|
||||
.get(normalized, orgId) as { id: string } | undefined;
|
||||
if (!workspace) throw new Error("Provider workspace context was not found.");
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private loadConfiguredProvider(orgId: string, providerId: string): ProviderRow {
|
||||
const provider = this.db.prepare(`
|
||||
select id, provider_key, auth_type, model, credential_id, enabled, auth_state,
|
||||
@@ -292,6 +308,7 @@ export class ProviderBackgroundService {
|
||||
}
|
||||
|
||||
interface PreparedBackgroundExecution {
|
||||
workspaceId: string | null;
|
||||
prompt: string;
|
||||
instructions: string | null;
|
||||
provider: ProviderRow & { model: string };
|
||||
|
||||
@@ -7,12 +7,14 @@ export interface ProviderRequestOwner {
|
||||
id: string;
|
||||
provider_key: string;
|
||||
data_policy: string;
|
||||
workspace_scope: string;
|
||||
}
|
||||
|
||||
export interface CreateProviderRequestInput {
|
||||
orgId: string;
|
||||
provider: ProviderRequestOwner;
|
||||
model: string;
|
||||
workspaceId: string | null;
|
||||
promptSha256: string;
|
||||
inputCharCount: number;
|
||||
executionMode: ProviderExecutionMode;
|
||||
@@ -99,23 +101,24 @@ export class ProviderRequestStore {
|
||||
const requestId = createId("preq");
|
||||
this.db.prepare(`
|
||||
insert into provider_requests(
|
||||
id, org_id, provider_id, provider_key, model, prompt_sha256,
|
||||
id, org_id, provider_id, provider_key, workspace_id, model, prompt_sha256,
|
||||
response_text, raw_response_json, state, error_message, input_char_count,
|
||||
output_char_count, execution_mode, policy_decision, external_response_id,
|
||||
stream_event_count, cancelled_at, created_at, completed_at
|
||||
)
|
||||
values (?, ?, ?, ?, ?, ?, null, null, ?, null, ?, 0, ?, ?, null, 0, null, ?, null)
|
||||
values (?, ?, ?, ?, ?, ?, ?, null, null, ?, null, ?, 0, ?, ?, null, 0, null, ?, null)
|
||||
`).run(
|
||||
requestId,
|
||||
input.orgId,
|
||||
input.provider.id,
|
||||
input.provider.provider_key,
|
||||
input.workspaceId,
|
||||
input.model,
|
||||
input.promptSha256,
|
||||
PROVIDER_REQUEST_STATE.PENDING,
|
||||
input.inputCharCount,
|
||||
input.executionMode,
|
||||
`allowed:${input.provider.data_policy}`,
|
||||
policyDecision(input.provider, input.workspaceId),
|
||||
input.createdAt,
|
||||
);
|
||||
return requestId;
|
||||
@@ -254,3 +257,8 @@ export class ProviderRequestStore {
|
||||
`).run(createId("puw"), orgId, providerId, window.start, window.end, inputCharCount, outputCharCount, now, now);
|
||||
}
|
||||
}
|
||||
|
||||
function policyDecision(provider: ProviderRequestOwner, workspaceId: string | null): string {
|
||||
if (provider.workspace_scope === "workspace") return `allowed:${provider.data_policy}:workspace:${workspaceId}`;
|
||||
return `allowed:${provider.data_policy}:organization`;
|
||||
}
|
||||
@@ -361,14 +361,16 @@ export class ProviderService {
|
||||
const prompt = input.prompt.trim();
|
||||
if (!prompt) throw new Error("Prompt is required.");
|
||||
const provider = this.loadConfiguredProvider(input.orgId, input.providerId);
|
||||
const workspaceId = this.normalizeWorkspaceId(input.orgId, input.workspaceId ?? null);
|
||||
const executionMode = input.executionMode ?? PROVIDER_EXECUTION_MODE.FOREGROUND;
|
||||
this.enforcePolicy(input.orgId, provider, executionMode);
|
||||
this.enforcePolicy(input.orgId, provider, executionMode, workspaceId);
|
||||
if (provider.auth_type !== PROVIDER_AUTH_TYPE.API_KEY) {
|
||||
throw new Error("Provider response execution requires an API key adapter.");
|
||||
}
|
||||
if (!provider.model) throw new Error("Provider model is required.");
|
||||
return {
|
||||
orgId: input.orgId,
|
||||
workspaceId,
|
||||
prompt,
|
||||
instructions: input.instructions?.trim() || null,
|
||||
executionMode,
|
||||
@@ -383,6 +385,7 @@ export class ProviderService {
|
||||
orgId: execution.orgId,
|
||||
provider: execution.provider,
|
||||
model: execution.provider.model,
|
||||
workspaceId: execution.workspaceId,
|
||||
promptSha256: hashPrompt(execution.prompt),
|
||||
inputCharCount: execution.prompt.length,
|
||||
executionMode: execution.executionMode,
|
||||
@@ -390,8 +393,12 @@ export class ProviderService {
|
||||
});
|
||||
}
|
||||
|
||||
private enforcePolicy(orgId: string, provider: ProviderRow, executionMode: ProviderExecutionMode): void {
|
||||
private enforcePolicy(orgId: string, provider: ProviderRow, executionMode: ProviderExecutionMode, workspaceId: string | null): void {
|
||||
if (!Object.values(PROVIDER_EXECUTION_MODE).includes(executionMode)) throw new Error("Provider execution mode is invalid.");
|
||||
if (!WORKSPACE_SCOPES.has(provider.workspace_scope)) throw new Error("Provider workspace scope is invalid.");
|
||||
if (provider.workspace_scope === "workspace" && !workspaceId) {
|
||||
throw new Error("Provider workspace scope requires a workspace context.");
|
||||
}
|
||||
if (executionMode === PROVIDER_EXECUTION_MODE.BACKGROUND && provider.data_policy === PROVIDER_DATA_POLICY.ZERO_DATA_RETENTION) {
|
||||
throw new Error("Background execution conflicts with zero data retention policy.");
|
||||
}
|
||||
@@ -411,6 +418,15 @@ export class ProviderService {
|
||||
if (requestCount >= provider.monthly_request_limit) throw new Error("Provider monthly request limit has been reached.");
|
||||
}
|
||||
|
||||
private normalizeWorkspaceId(orgId: string, workspaceId: string | null): string | null {
|
||||
const normalized = workspaceId?.trim() || null;
|
||||
if (!normalized) return null;
|
||||
const workspace = this.db.prepare("select id from workspaces where id = ? and org_id = ?")
|
||||
.get(normalized, orgId) as { id: string } | undefined;
|
||||
if (!workspace) throw new Error("Provider workspace context was not found.");
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private loadConfiguredProvider(orgId: string, providerId: string): ProviderRow {
|
||||
const provider = this.db.prepare(`
|
||||
select id, provider_key, auth_type, model, credential_id, enabled, auth_state,
|
||||
@@ -476,6 +492,7 @@ export class ProviderService {
|
||||
|
||||
interface PreparedProviderExecution {
|
||||
orgId: string;
|
||||
workspaceId: string | null;
|
||||
prompt: string;
|
||||
instructions: string | null;
|
||||
executionMode: ProviderExecutionMode;
|
||||
|
||||
@@ -262,7 +262,7 @@ export class SdlcService {
|
||||
{ key: "provider_adapter_coverage", passed: missing.length === 0, value: `${adapterKeys.length}/${catalogKeys.length}` },
|
||||
{ key: "provider_rate_limit_registry", passed: rateLimitRegistry === 1, value: rateLimitRegistry },
|
||||
{ key: "provider_runtime_contract", passed: runtimeContractReady, value: runtimeContractReady },
|
||||
{ key: "schema_version", passed: schemaVersion >= 30, value: schemaVersion },
|
||||
{ key: "schema_version", passed: schemaVersion >= 31, value: schemaVersion },
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -88,6 +88,7 @@ export class TaskExecutionService {
|
||||
const requestId = await this.providers.createResponse({
|
||||
orgId: input.orgId,
|
||||
providerId: provider.id,
|
||||
workspaceId: task.workspace_id,
|
||||
prompt: buildTaskPrompt({
|
||||
task,
|
||||
agent,
|
||||
|
||||
@@ -189,6 +189,7 @@ export interface UpdateProviderPolicyInput {
|
||||
export interface CreateProviderResponseInput {
|
||||
orgId: string;
|
||||
providerId: string;
|
||||
workspaceId?: string | null;
|
||||
prompt: string;
|
||||
instructions: string | null;
|
||||
executionMode?: ProviderExecutionMode;
|
||||
|
||||
@@ -15,6 +15,7 @@ export interface ProviderRequestRecord {
|
||||
orgId: string;
|
||||
providerId: string;
|
||||
providerKey: string;
|
||||
workspaceId: string | null;
|
||||
model: string;
|
||||
promptSha256: string;
|
||||
responseText: string | null;
|
||||
|
||||
@@ -23,7 +23,7 @@ describe("domain services", () => {
|
||||
const connection = createConnection();
|
||||
|
||||
const rows = connection.db.prepare("select version from schema_migrations order by version asc").all() as Array<{ version: number }>;
|
||||
expect(rows.map((row) => row.version)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30]);
|
||||
expect(rows.map((row) => row.version)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31]);
|
||||
connection.close();
|
||||
});
|
||||
|
||||
|
||||
@@ -149,7 +149,7 @@ describe("migration service", () => {
|
||||
create table chat_presence(principal_type text, principal_id text);
|
||||
create table chat_read_state(org_id text, employee_id text, space_id text, last_read_message_id text);
|
||||
create table providers(id text primary key, org_id text, credential_id text);
|
||||
create table provider_requests(id text primary key, org_id text, provider_id text);
|
||||
create table provider_requests(id text primary key, org_id text, provider_id text, workspace_id text);
|
||||
create table provider_usage_windows(id text primary key, org_id text, provider_id text);
|
||||
create table provider_rate_limit_observations(id text primary key, org_id text, provider_id text);
|
||||
create table credentials(id text primary key, org_id text);
|
||||
@@ -225,7 +225,7 @@ describe("migration service", () => {
|
||||
insert into board_interventions(id, org_id, thread_id, message_id, approval_id) values ('bdi-1', 'org-1', 'thread-1', 'msg-1', 'approval-1');
|
||||
insert into chat_presence(principal_type, principal_id) values ('agent', 'agent-1');
|
||||
insert into chat_read_state(org_id, employee_id, space_id, last_read_message_id) values ('org-1', 'emp-1', 'space-1', 'msg-1');
|
||||
insert into provider_requests(id, org_id, provider_id) values ('req-1', 'org-1', 'prov-1');
|
||||
insert into provider_requests(id, org_id, provider_id, workspace_id) values ('req-1', 'org-1', 'prov-1', 'wsp-1');
|
||||
insert into provider_usage_windows(id, org_id, provider_id) values ('usage-1', 'org-1', 'prov-1');
|
||||
insert into provider_rate_limit_observations(id, org_id, provider_id) values ('quota-1', 'org-1', 'prov-1');
|
||||
insert into skill_definitions(id, org_id) values ('skill-1', 'org-1');
|
||||
|
||||
@@ -142,11 +142,11 @@ describe("provider service streaming", () => {
|
||||
providers.saveCredential({ orgId, providerId: openAiProviderId, secret: "sk-live-provider", model: "gpt-5.4" });
|
||||
connection.db.prepare(`
|
||||
insert into provider_requests(
|
||||
id, org_id, provider_id, provider_key, model, prompt_sha256,
|
||||
id, org_id, provider_id, provider_key, workspace_id, model, prompt_sha256,
|
||||
response_text, raw_response_json, state, error_message, input_char_count,
|
||||
output_char_count, execution_mode, policy_decision, external_response_id,
|
||||
stream_event_count, cancelled_at, created_at, completed_at
|
||||
) values (?, ?, ?, 'openai', 'gpt-5.4', ?, 'partial', '[]', ?, null, 6, 7, ?, 'allowed:standard', ?, 2, null, ?, null)
|
||||
) values (?, ?, ?, 'openai', null, 'gpt-5.4', ?, 'partial', '[]', ?, null, 6, 7, ?, 'allowed:standard:organization', ?, 2, null, ?, null)
|
||||
`).run(
|
||||
requestId,
|
||||
orgId,
|
||||
|
||||
@@ -380,7 +380,7 @@ describe("provider service", () => {
|
||||
inputCharCount: 16,
|
||||
outputCharCount: 25,
|
||||
executionMode: PROVIDER_EXECUTION_MODE.FOREGROUND,
|
||||
policyDecision: "allowed:standard",
|
||||
policyDecision: "allowed:standard:organization",
|
||||
});
|
||||
expect(state.providerRateLimits.find((item) => item.providerId === openAiProviderId)).toMatchObject({
|
||||
providerId: openAiProviderId,
|
||||
@@ -394,6 +394,68 @@ describe("provider service", () => {
|
||||
connection.close();
|
||||
});
|
||||
|
||||
test("requires workspace context for workspace-scoped provider execution", async () => {
|
||||
const connection = createConnection();
|
||||
const organization = new OrganizationService(connection.db);
|
||||
const snapshot = new SnapshotService(connection.db);
|
||||
const adapters: ProviderAdapterRegistry = {
|
||||
...providerAdapters,
|
||||
openai: {
|
||||
...providerAdapters.openai,
|
||||
parseAuthProfile: (secret) => ({ type: "api_key", apiKey: secret.trim() }),
|
||||
checkHealth: async () => ({ checkedAt: new Date().toISOString(), rateLimitState: null }),
|
||||
createResponse: async ({ prompt }) => ({ text: `accepted:${prompt}`, rawResponseJson: "{}", rateLimitState: null }),
|
||||
},
|
||||
};
|
||||
const providers = new ProviderService(connection.db, adapters, testVault);
|
||||
let orgId = "";
|
||||
let openAiProviderId = "";
|
||||
let workspaceId = "";
|
||||
|
||||
connection.transaction(() => {
|
||||
orgId = createOrganization(organization);
|
||||
openAiProviderId = providerId(connection, orgId, "openai");
|
||||
workspaceId = "wsp_release";
|
||||
const now = new Date().toISOString();
|
||||
connection.db.prepare(`
|
||||
insert into workspaces(id, org_id, name, local_path, repo_url, repo_ref, project_id, is_primary, created_at, updated_at)
|
||||
values (?, ?, ?, ?, ?, ?, null, ?, ?, ?)
|
||||
`).run(workspaceId, orgId, "Release", "C:/work/release", "https://example.com/release.git", "main", 1, now, now);
|
||||
providers.saveCredential({ orgId, providerId: openAiProviderId, secret: "sk-live-provider", model: "gpt-5.4" });
|
||||
providers.updatePolicy({
|
||||
orgId,
|
||||
providerId: openAiProviderId,
|
||||
dataPolicy: PROVIDER_DATA_POLICY.STANDARD,
|
||||
backgroundEnabled: false,
|
||||
monthlyRequestLimit: 10,
|
||||
workspaceScope: "workspace",
|
||||
});
|
||||
});
|
||||
|
||||
await expect(providers.createResponse({
|
||||
orgId,
|
||||
providerId: openAiProviderId,
|
||||
prompt: "Draft workspace memo",
|
||||
instructions: null,
|
||||
})).rejects.toThrow("Provider workspace scope requires a workspace context.");
|
||||
expect(connection.db.prepare("select count(*) as count from provider_requests").get()).toEqual({ count: 0 });
|
||||
|
||||
await providers.createResponse({
|
||||
orgId,
|
||||
providerId: openAiProviderId,
|
||||
workspaceId,
|
||||
prompt: "Draft workspace memo",
|
||||
instructions: null,
|
||||
});
|
||||
|
||||
expect(snapshot.load().providerRequests[0]).toMatchObject({
|
||||
providerId: openAiProviderId,
|
||||
workspaceId,
|
||||
policyDecision: `allowed:standard:workspace:${workspaceId}`,
|
||||
});
|
||||
connection.close();
|
||||
});
|
||||
|
||||
test("blocks background execution under zero data retention policy", async () => {
|
||||
const connection = createConnection();
|
||||
const organization = new OrganizationService(connection.db);
|
||||
|
||||
@@ -50,6 +50,7 @@ describe("task execution service", () => {
|
||||
let orgId = "";
|
||||
let providerId = "";
|
||||
let taskId = "";
|
||||
let workspaceId = "";
|
||||
|
||||
connection.transaction(() => {
|
||||
orgId = organization.createOrganization({
|
||||
@@ -60,6 +61,12 @@ describe("task execution service", () => {
|
||||
});
|
||||
providerId = openAiProviderId(connection, orgId);
|
||||
providers.saveCredential({ orgId, providerId, secret: "sk-live-provider", model: "gpt-5.4" });
|
||||
workspaceId = "wsp_task_execution";
|
||||
const now = new Date().toISOString();
|
||||
connection.db.prepare(`
|
||||
insert into workspaces(id, org_id, name, local_path, repo_url, repo_ref, project_id, is_primary, created_at, updated_at)
|
||||
values (?, ?, ?, ?, ?, ?, null, ?, ?, ?)
|
||||
`).run(workspaceId, orgId, "Task Workspace", "C:/work/task", "https://example.com/task.git", "main", 1, now, now);
|
||||
const agentId = organization.createAgent({
|
||||
orgId,
|
||||
name: "Ada",
|
||||
@@ -79,7 +86,7 @@ describe("task execution service", () => {
|
||||
risk: RISK_LEVEL.MEDIUM,
|
||||
projectId: null,
|
||||
goalId: null,
|
||||
workspaceId: null,
|
||||
workspaceId,
|
||||
priority: TASK_PRIORITY.HIGH,
|
||||
requiresFilesystem: false,
|
||||
});
|
||||
@@ -91,9 +98,10 @@ describe("task execution service", () => {
|
||||
const request = state.providerRequests.find((item) => item.id === run?.providerRequestId);
|
||||
|
||||
expect(run).toMatchObject({ taskId, agentId: expect.any(String), providerId, state: "completed", errorMessage: null });
|
||||
expect(request).toMatchObject({ providerId, state: "succeeded", responseText: "executed:gpt-5.4" });
|
||||
expect(request).toMatchObject({ providerId, workspaceId, state: "succeeded", responseText: "executed:gpt-5.4" });
|
||||
expect(capturedPrompt).toContain("Task: Draft launch checklist");
|
||||
expect(capturedPrompt).toContain("Assignee: Ada (Execution lead)");
|
||||
expect(capturedPrompt).toContain("Workspace: Task Workspace");
|
||||
expect(state.auditEvents.some((event) => event.entityType === "task_run" && event.action === "completed")).toBe(true);
|
||||
connection.close();
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user