Add SDLC review and eval controls
This commit is contained in:
1 parent
bc14a2fbcb
commit
bbf90a1a62
19 files changed
+927
-2
No files matched your search
@@ -17,6 +17,7 @@ import { ComplianceService } from "../services/compliance-service";
|
||||
import { SearchService } from "../services/search-service";
|
||||
import { IdentityAccessService } from "../services/identity-access-service";
|
||||
import { OperationsService } from "../services/operations-service";
|
||||
import { SdlcService } from "../services/sdlc-service";
|
||||
|
||||
export interface IpcDeps {
|
||||
connection: DatabaseConnection;
|
||||
@@ -38,6 +39,7 @@ export function registerIpcHandlers(deps: IpcDeps): void {
|
||||
const search = new SearchService(deps.connection.db);
|
||||
const identityAccess = new IdentityAccessService(deps.connection.db);
|
||||
const operations = new OperationsService(deps.connection.db);
|
||||
const sdlc = new SdlcService(deps.connection.db);
|
||||
const onboarding = new OnboardingService({
|
||||
db: deps.connection.db,
|
||||
saveProviderCredential: (input) => providers.saveCredential(input),
|
||||
@@ -157,6 +159,9 @@ export function registerIpcHandlers(deps: IpcDeps): void {
|
||||
ipcMain.handle(IPC_CHANNELS.createMigrationReport, (_event, input) => mutate(() => migrations.createReport(input)));
|
||||
ipcMain.handle(IPC_CHANNELS.setFeatureFlag, (_event, input) => mutate(() => operations.setFeatureFlag(input)));
|
||||
ipcMain.handle(IPC_CHANNELS.runChaosDrill, (_event, input) => mutate(() => operations.runChaosDrill(input)));
|
||||
ipcMain.handle(IPC_CHANNELS.runSdlcReview, (_event, input) => mutate(() => sdlc.runReview(input)));
|
||||
ipcMain.handle(IPC_CHANNELS.runEvalSuite, (_event, input) => mutate(() => sdlc.runEvalSuite(input)));
|
||||
ipcMain.handle(IPC_CHANNELS.setPrBotGate, (_event, input) => mutate(() => sdlc.setPrBotGate(input)));
|
||||
ipcMain.handle(IPC_CHANNELS.createComplianceExport, (_event, input) => mutate(() => compliance.createExport(input)));
|
||||
ipcMain.handle(IPC_CHANNELS.createLegalHold, (_event, input) => mutate(() => compliance.createLegalHold(input)));
|
||||
ipcMain.handle(IPC_CHANNELS.setLegalHoldState, (_event, input) => mutate(() => compliance.setLegalHoldState(input)));
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
export const sdlcControlsSchema = `
|
||||
create table if not exists sdlc_review_agents (
|
||||
id text primary key,
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
key text not null,
|
||||
label text not null,
|
||||
specialty text not null,
|
||||
enabled integer not null check(enabled in (0, 1)),
|
||||
created_at text not null,
|
||||
updated_at text not null,
|
||||
unique(org_id, key)
|
||||
);
|
||||
|
||||
create table if not exists sdlc_review_runs (
|
||||
id text primary key,
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
review_agent_id text not null references sdlc_review_agents(id) on delete cascade,
|
||||
target_type text not null,
|
||||
target_ref text not null,
|
||||
state text not null,
|
||||
result_json text not null,
|
||||
created_at text not null,
|
||||
completed_at text not null
|
||||
);
|
||||
|
||||
create table if not exists eval_suites (
|
||||
id text primary key,
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
key text not null,
|
||||
label text not null,
|
||||
scope text not null,
|
||||
created_at text not null,
|
||||
updated_at text not null,
|
||||
unique(org_id, key)
|
||||
);
|
||||
|
||||
create table if not exists eval_cases (
|
||||
id text primary key,
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
suite_id text not null references eval_suites(id) on delete cascade,
|
||||
title text not null,
|
||||
source_type text not null,
|
||||
source_ref text not null,
|
||||
expected_signal text not null,
|
||||
created_at text not null,
|
||||
unique(suite_id, title)
|
||||
);
|
||||
|
||||
create table if not exists eval_runs (
|
||||
id text primary key,
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
suite_id text not null references eval_suites(id) on delete cascade,
|
||||
state text not null,
|
||||
passed_count integer not null,
|
||||
failed_count integer not null,
|
||||
result_json text not null,
|
||||
created_at text not null,
|
||||
completed_at text not null
|
||||
);
|
||||
|
||||
create table if not exists pr_bot_gates (
|
||||
id text primary key,
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
key text not null,
|
||||
label text not null,
|
||||
gate_type text not null,
|
||||
enabled integer not null check(enabled in (0, 1)),
|
||||
created_at text not null,
|
||||
updated_at text not null,
|
||||
unique(org_id, key)
|
||||
);
|
||||
|
||||
create index if not exists idx_sdlc_review_runs_org_agent_created
|
||||
on sdlc_review_runs(org_id, review_agent_id, created_at);
|
||||
|
||||
create index if not exists idx_eval_runs_org_suite_created
|
||||
on eval_runs(org_id, suite_id, created_at);
|
||||
|
||||
insert into sdlc_review_agents(id, org_id, key, label, specialty, enabled, created_at, updated_at)
|
||||
select 'sra_' || lower(hex(randomblob(16))), organizations.id, seed.key, seed.label, seed.specialty, 1, datetime('now'), datetime('now')
|
||||
from organizations
|
||||
cross join (
|
||||
select 'code_reviewer' as key, 'Code Review Agent' as label, 'static quality and architecture' as specialty
|
||||
union all select 'migration_reviewer', 'Migration Review Agent', 'schema diff and rollback evidence'
|
||||
union all select 'policy_reviewer', 'Policy Review Agent', 'provider, approval, and access policy'
|
||||
) seed
|
||||
where not exists (
|
||||
select 1 from sdlc_review_agents
|
||||
where sdlc_review_agents.org_id = organizations.id and sdlc_review_agents.key = seed.key
|
||||
);
|
||||
|
||||
insert into eval_suites(id, org_id, key, label, scope, created_at, updated_at)
|
||||
select 'evs_' || lower(hex(randomblob(16))), organizations.id, seed.key, seed.label, seed.scope, datetime('now'), datetime('now')
|
||||
from organizations
|
||||
cross join (
|
||||
select 'contract' as key, 'Contract checks' as label, 'provider adapters, event schemas, slash commands' as scope
|
||||
union all select 'integration', 'Integration checks', 'chat, approval, file, skill resolution flows'
|
||||
union all select 'replay', 'Replay checks', 'audit timeline and historical decision regression'
|
||||
) seed
|
||||
where not exists (
|
||||
select 1 from eval_suites
|
||||
where eval_suites.org_id = organizations.id and eval_suites.key = seed.key
|
||||
);
|
||||
|
||||
insert into eval_cases(id, org_id, suite_id, title, source_type, source_ref, expected_signal, created_at)
|
||||
select 'evc_' || lower(hex(randomblob(16))), eval_suites.org_id, eval_suites.id, seed.title, seed.source_type, seed.source_ref, seed.expected_signal, datetime('now')
|
||||
from eval_suites
|
||||
join (
|
||||
select 'contract' as suite_key, 'Provider catalog has adapters' as title, 'provider_gateway' as source_type, 'providers' as source_ref, 'adapter_coverage' as expected_signal
|
||||
union all select 'contract', 'Schema ledger is current', 'database', 'schema_migrations', 'migration_ledger'
|
||||
union all select 'integration', 'Approval routes are assigned', 'org_governance', 'approval_assignments', 'route_coverage'
|
||||
union all select 'integration', 'Skills can resolve runtime snapshots', 'skills_platform', 'skill_runtime_snapshots', 'skill_resolution'
|
||||
union all select 'replay', 'Audit timeline has domain events', 'audit', 'audit_events', 'replayable_timeline'
|
||||
union all select 'replay', 'Operational signals are captured', 'operations', 'operational_events', 'slo_signal'
|
||||
) seed on seed.suite_key = eval_suites.key
|
||||
where not exists (
|
||||
select 1 from eval_cases
|
||||
where eval_cases.suite_id = eval_suites.id and eval_cases.title = seed.title
|
||||
);
|
||||
|
||||
insert into pr_bot_gates(id, org_id, key, label, gate_type, enabled, created_at, updated_at)
|
||||
select 'pbg_' || lower(hex(randomblob(16))), organizations.id, seed.key, seed.label, seed.gate_type, 1, datetime('now'), datetime('now')
|
||||
from organizations
|
||||
cross join (
|
||||
select 'static_checks' as key, 'Static checks' as label, 'typecheck_lint_test' as gate_type
|
||||
union all select 'migration_diff', 'Migration diff' as label, 'schema_review' as gate_type
|
||||
union all select 'policy_diff', 'Policy diff' as label, 'policy_review' as gate_type
|
||||
union all select 'human_approval', 'Human approval' as label, 'approval_required' as gate_type
|
||||
) seed
|
||||
where not exists (
|
||||
select 1 from pr_bot_gates
|
||||
where pr_bot_gates.org_id = organizations.id and pr_bot_gates.key = seed.key
|
||||
);
|
||||
|
||||
insert into schema_migrations(version, name, applied_at)
|
||||
values (18, 'sdlc_controls', datetime('now'));
|
||||
`;
|
||||
@@ -16,6 +16,7 @@ import { providerPolicyUsageSchema } from "./migrations/014-provider-policy-usag
|
||||
import { identityAccessSchema } from "./migrations/015-identity-access";
|
||||
import { orgGovernanceSchema } from "./migrations/016-org-governance";
|
||||
import { operationsControlsSchema } from "./migrations/017-operations-controls";
|
||||
import { sdlcControlsSchema } from "./migrations/018-sdlc-controls";
|
||||
|
||||
interface Migration {
|
||||
version: number;
|
||||
@@ -41,6 +42,7 @@ const MIGRATIONS: Migration[] = [
|
||||
{ version: 15, name: "identity_access", sql: identityAccessSchema },
|
||||
{ version: 16, name: "org_governance", sql: orgGovernanceSchema },
|
||||
{ version: 17, name: "operations_controls", sql: operationsControlsSchema },
|
||||
{ version: 18, name: "sdlc_controls", sql: sdlcControlsSchema },
|
||||
];
|
||||
|
||||
const SCHEMA_VERSION = MIGRATIONS[MIGRATIONS.length - 1].version;
|
||||
|
||||
@@ -31,6 +31,12 @@ const LEGACY_TABLES = [
|
||||
"operational_events",
|
||||
"chaos_drills",
|
||||
"chaos_drill_runs",
|
||||
"sdlc_review_agents",
|
||||
"sdlc_review_runs",
|
||||
"eval_suites",
|
||||
"eval_cases",
|
||||
"eval_runs",
|
||||
"pr_bot_gates",
|
||||
"chat_spaces",
|
||||
"chat_messages",
|
||||
"chat_threads",
|
||||
@@ -142,6 +148,15 @@ const RELATION_CHECKS: RelationCheck[] = [
|
||||
{ table: "chaos_drills", column: "org_id", referencedTable: "organizations", referencedColumn: "id", reason: "dangling_org_id" },
|
||||
{ table: "chaos_drill_runs", column: "org_id", referencedTable: "organizations", referencedColumn: "id", reason: "dangling_org_id" },
|
||||
{ table: "chaos_drill_runs", column: "drill_id", referencedTable: "chaos_drills", referencedColumn: "id", reason: "dangling_drill_id" },
|
||||
{ table: "sdlc_review_agents", column: "org_id", referencedTable: "organizations", referencedColumn: "id", reason: "dangling_org_id" },
|
||||
{ table: "sdlc_review_runs", column: "org_id", referencedTable: "organizations", referencedColumn: "id", reason: "dangling_org_id" },
|
||||
{ table: "sdlc_review_runs", column: "review_agent_id", referencedTable: "sdlc_review_agents", referencedColumn: "id", reason: "dangling_review_agent_id" },
|
||||
{ table: "eval_suites", column: "org_id", referencedTable: "organizations", referencedColumn: "id", reason: "dangling_org_id" },
|
||||
{ table: "eval_cases", column: "org_id", referencedTable: "organizations", referencedColumn: "id", reason: "dangling_org_id" },
|
||||
{ table: "eval_cases", column: "suite_id", referencedTable: "eval_suites", referencedColumn: "id", reason: "dangling_suite_id" },
|
||||
{ table: "eval_runs", column: "org_id", referencedTable: "organizations", referencedColumn: "id", reason: "dangling_org_id" },
|
||||
{ table: "eval_runs", column: "suite_id", referencedTable: "eval_suites", referencedColumn: "id", reason: "dangling_suite_id" },
|
||||
{ table: "pr_bot_gates", column: "org_id", referencedTable: "organizations", referencedColumn: "id", reason: "dangling_org_id" },
|
||||
{ table: "chat_spaces", column: "org_id", referencedTable: "organizations", referencedColumn: "id", reason: "dangling_org_id" },
|
||||
{ table: "chat_messages", column: "org_id", referencedTable: "organizations", referencedColumn: "id", reason: "dangling_org_id" },
|
||||
{ table: "chat_messages", column: "space_id", referencedTable: "chat_spaces", referencedColumn: "id", reason: "dangling_space_id" },
|
||||
|
||||
@@ -8,6 +8,7 @@ import { PolicyService } from "./policy-service";
|
||||
import { IdentityAccessService } from "./identity-access-service";
|
||||
import { OrgGovernanceService } from "./org-governance-service";
|
||||
import { OperationsService } from "./operations-service";
|
||||
import { SdlcService } from "./sdlc-service";
|
||||
import { assertUsableMentionName } from "./mention-handles";
|
||||
|
||||
export class OrganizationService {
|
||||
@@ -16,6 +17,7 @@ export class OrganizationService {
|
||||
private readonly identityAccess: IdentityAccessService;
|
||||
private readonly governance: OrgGovernanceService;
|
||||
private readonly operations: OperationsService;
|
||||
private readonly sdlc: SdlcService;
|
||||
|
||||
constructor(private readonly db: DatabaseSync) {
|
||||
this.audit = new AuditService(db);
|
||||
@@ -23,6 +25,7 @@ export class OrganizationService {
|
||||
this.identityAccess = new IdentityAccessService(db);
|
||||
this.governance = new OrgGovernanceService(db);
|
||||
this.operations = new OperationsService(db);
|
||||
this.sdlc = new SdlcService(db);
|
||||
}
|
||||
|
||||
createOrganization(input: CreateOrganizationInput): string {
|
||||
@@ -52,6 +55,7 @@ export class OrganizationService {
|
||||
this.identityAccess.seedEmployeeAccess(orgId, employeeId, now);
|
||||
this.governance.seedOrganizationGovernance(orgId, employeeId, now);
|
||||
this.operations.seedOrganizationOperations(orgId, now);
|
||||
this.sdlc.seedOrganizationSdlc(orgId, now);
|
||||
|
||||
for (const space of [
|
||||
["company", "general", "Company coordination", "workspace"],
|
||||
|
||||
@@ -0,0 +1,292 @@
|
||||
import type { DatabaseSync } from "node:sqlite";
|
||||
import type { RunEvalSuiteInput, RunSdlcReviewInput, SetPrBotGateInput } from "../../shared/commands";
|
||||
import {
|
||||
EVAL_SUITE_KEY,
|
||||
PR_BOT_GATE_KEY,
|
||||
SDLC_REVIEW_AGENT_KEY,
|
||||
SDLC_REVIEW_TARGET_TYPE,
|
||||
SDLC_RUN_STATE,
|
||||
type EvalSuiteKey,
|
||||
type SdlcReviewAgentKey,
|
||||
type SdlcReviewTargetType,
|
||||
} from "../../shared/domain";
|
||||
import { PROVIDER_CATALOG } from "../../shared/provider-catalog";
|
||||
import { createId } from "../../shared/ids";
|
||||
import { AuditService } from "./audit-service";
|
||||
import { providerAdapters } from "./provider-adapters";
|
||||
|
||||
const REVIEW_AGENTS = [
|
||||
{ key: SDLC_REVIEW_AGENT_KEY.CODE_REVIEWER, label: "Code Review Agent", specialty: "static quality and architecture" },
|
||||
{ key: SDLC_REVIEW_AGENT_KEY.MIGRATION_REVIEWER, label: "Migration Review Agent", specialty: "schema diff and rollback evidence" },
|
||||
{ key: SDLC_REVIEW_AGENT_KEY.POLICY_REVIEWER, label: "Policy Review Agent", specialty: "provider, approval, and access policy" },
|
||||
] as const;
|
||||
|
||||
const EVAL_SUITES = [
|
||||
{ key: EVAL_SUITE_KEY.CONTRACT, label: "Contract checks", scope: "provider adapters, event schemas, slash commands" },
|
||||
{ key: EVAL_SUITE_KEY.INTEGRATION, label: "Integration checks", scope: "chat, approval, file, skill resolution flows" },
|
||||
{ key: EVAL_SUITE_KEY.REPLAY, label: "Replay checks", scope: "audit timeline and historical decision regression" },
|
||||
] as const;
|
||||
|
||||
const EVAL_CASES = [
|
||||
{ suiteKey: EVAL_SUITE_KEY.CONTRACT, title: "Provider catalog has adapters", sourceType: "provider_gateway", sourceRef: "providers", expectedSignal: "adapter_coverage" },
|
||||
{ suiteKey: EVAL_SUITE_KEY.CONTRACT, title: "Schema ledger is current", sourceType: "database", sourceRef: "schema_migrations", expectedSignal: "migration_ledger" },
|
||||
{ suiteKey: EVAL_SUITE_KEY.INTEGRATION, title: "Approval routes are assigned", sourceType: "org_governance", sourceRef: "approval_assignments", expectedSignal: "route_coverage" },
|
||||
{ suiteKey: EVAL_SUITE_KEY.INTEGRATION, title: "Skills can resolve runtime snapshots", sourceType: "skills_platform", sourceRef: "skill_runtime_snapshots", expectedSignal: "skill_resolution" },
|
||||
{ suiteKey: EVAL_SUITE_KEY.REPLAY, title: "Audit timeline has domain events", sourceType: "audit", sourceRef: "audit_events", expectedSignal: "replayable_timeline" },
|
||||
{ suiteKey: EVAL_SUITE_KEY.REPLAY, title: "Operational signals are captured", sourceType: "operations", sourceRef: "operational_events", expectedSignal: "slo_signal" },
|
||||
] as const;
|
||||
|
||||
const PR_BOT_GATES = [
|
||||
{ key: PR_BOT_GATE_KEY.STATIC_CHECKS, label: "Static checks", gateType: "typecheck_lint_test" },
|
||||
{ key: PR_BOT_GATE_KEY.MIGRATION_DIFF, label: "Migration diff", gateType: "schema_review" },
|
||||
{ key: PR_BOT_GATE_KEY.POLICY_DIFF, label: "Policy diff", gateType: "policy_review" },
|
||||
{ key: PR_BOT_GATE_KEY.HUMAN_APPROVAL, label: "Human approval", gateType: "approval_required" },
|
||||
] as const;
|
||||
|
||||
interface ReviewAgentRow {
|
||||
id: string;
|
||||
key: SdlcReviewAgentKey;
|
||||
enabled: number;
|
||||
}
|
||||
|
||||
interface EvalSuiteRow {
|
||||
id: string;
|
||||
key: EvalSuiteKey;
|
||||
}
|
||||
|
||||
interface GateCheck {
|
||||
key: string;
|
||||
passed: boolean;
|
||||
value: number | string | boolean;
|
||||
}
|
||||
|
||||
export class SdlcService {
|
||||
private readonly audit: AuditService;
|
||||
|
||||
constructor(private readonly db: DatabaseSync) {
|
||||
this.audit = new AuditService(db);
|
||||
}
|
||||
|
||||
seedOrganizationSdlc(orgId: string, now: string): void {
|
||||
for (const agent of REVIEW_AGENTS) {
|
||||
this.db.prepare(`
|
||||
insert into sdlc_review_agents(id, org_id, key, label, specialty, enabled, created_at, updated_at)
|
||||
values (?, ?, ?, ?, ?, 1, ?, ?)
|
||||
on conflict(org_id, key) do nothing
|
||||
`).run(createId("sra"), orgId, agent.key, agent.label, agent.specialty, now, now);
|
||||
}
|
||||
for (const suite of EVAL_SUITES) {
|
||||
this.db.prepare(`
|
||||
insert into eval_suites(id, org_id, key, label, scope, created_at, updated_at)
|
||||
values (?, ?, ?, ?, ?, ?, ?)
|
||||
on conflict(org_id, key) do nothing
|
||||
`).run(createId("evs"), orgId, suite.key, suite.label, suite.scope, now, now);
|
||||
}
|
||||
for (const item of EVAL_CASES) {
|
||||
const suite = this.requireSuiteByKey(orgId, item.suiteKey);
|
||||
this.db.prepare(`
|
||||
insert into eval_cases(id, org_id, suite_id, title, source_type, source_ref, expected_signal, created_at)
|
||||
values (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
on conflict(suite_id, title) do nothing
|
||||
`).run(createId("evc"), orgId, suite.id, item.title, item.sourceType, item.sourceRef, item.expectedSignal, now);
|
||||
}
|
||||
for (const gate of PR_BOT_GATES) {
|
||||
this.db.prepare(`
|
||||
insert into pr_bot_gates(id, org_id, key, label, gate_type, enabled, created_at, updated_at)
|
||||
values (?, ?, ?, ?, ?, 1, ?, ?)
|
||||
on conflict(org_id, key) do nothing
|
||||
`).run(createId("pbg"), orgId, gate.key, gate.label, gate.gateType, now, now);
|
||||
}
|
||||
}
|
||||
|
||||
runReview(input: RunSdlcReviewInput): string {
|
||||
const agent = this.db.prepare("select id, key, enabled from sdlc_review_agents where id = ? and org_id = ?")
|
||||
.get(input.reviewAgentId, input.orgId) as ReviewAgentRow | undefined;
|
||||
if (!agent) throw new Error("SDLC review agent was not found.");
|
||||
if (agent.enabled !== 1) throw new Error("SDLC review agent is disabled.");
|
||||
this.assertTarget(input.orgId, input.targetType, input.targetRef);
|
||||
const checks = this.reviewChecks(input.orgId, agent.key, input.targetType);
|
||||
const state = checks.every((check) => check.passed) ? SDLC_RUN_STATE.PASSED : SDLC_RUN_STATE.FAILED;
|
||||
const id = createId("srr");
|
||||
const now = new Date().toISOString();
|
||||
this.db.prepare(`
|
||||
insert into sdlc_review_runs(id, org_id, review_agent_id, target_type, target_ref, state, result_json, created_at, completed_at)
|
||||
values (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(id, input.orgId, input.reviewAgentId, input.targetType, input.targetRef, state, JSON.stringify({ checks }), now, now);
|
||||
this.audit.record({
|
||||
orgId: input.orgId,
|
||||
entityType: "sdlc_review_run",
|
||||
entityId: id,
|
||||
actorType: "system",
|
||||
actorId: null,
|
||||
action: "created",
|
||||
payload: { reviewAgentKey: agent.key, state, targetType: input.targetType },
|
||||
});
|
||||
return id;
|
||||
}
|
||||
|
||||
runEvalSuite(input: RunEvalSuiteInput): string {
|
||||
const suite = this.db.prepare("select id, key from eval_suites where id = ? and org_id = ?")
|
||||
.get(input.suiteId, input.orgId) as EvalSuiteRow | undefined;
|
||||
if (!suite) throw new Error("Eval suite was not found.");
|
||||
const checks = this.evalChecks(input.orgId, suite.key);
|
||||
const passedCount = checks.filter((check) => check.passed).length;
|
||||
const failedCount = checks.length - passedCount;
|
||||
const state = failedCount === 0 ? SDLC_RUN_STATE.PASSED : SDLC_RUN_STATE.FAILED;
|
||||
const id = createId("evr");
|
||||
const now = new Date().toISOString();
|
||||
this.db.prepare(`
|
||||
insert into eval_runs(id, org_id, suite_id, state, passed_count, failed_count, result_json, created_at, completed_at)
|
||||
values (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(id, input.orgId, input.suiteId, state, passedCount, failedCount, JSON.stringify({ checks }), now, now);
|
||||
this.audit.record({
|
||||
orgId: input.orgId,
|
||||
entityType: "eval_run",
|
||||
entityId: id,
|
||||
actorType: "system",
|
||||
actorId: null,
|
||||
action: "created",
|
||||
payload: { suiteKey: suite.key, state, passedCount, failedCount },
|
||||
});
|
||||
return id;
|
||||
}
|
||||
|
||||
setPrBotGate(input: SetPrBotGateInput): void {
|
||||
const gate = this.db.prepare("select key from pr_bot_gates where id = ? and org_id = ?")
|
||||
.get(input.gateId, input.orgId) as { key: string } | undefined;
|
||||
if (!gate) throw new Error("PR bot gate was not found.");
|
||||
const now = new Date().toISOString();
|
||||
const result = this.db.prepare("update pr_bot_gates set enabled = ?, updated_at = ? where id = ? and org_id = ?")
|
||||
.run(input.enabled ? 1 : 0, now, input.gateId, input.orgId);
|
||||
if (result.changes === 0) throw new Error("PR bot gate was not found.");
|
||||
this.audit.record({
|
||||
orgId: input.orgId,
|
||||
entityType: "pr_bot_gate",
|
||||
entityId: input.gateId,
|
||||
actorType: "system",
|
||||
actorId: null,
|
||||
action: "updated",
|
||||
payload: { key: gate.key, enabled: input.enabled },
|
||||
});
|
||||
}
|
||||
|
||||
private requireSuiteByKey(orgId: string, key: EvalSuiteKey): { id: string } {
|
||||
const suite = this.db.prepare("select id from eval_suites where org_id = ? and key = ?")
|
||||
.get(orgId, key) as { id: string } | undefined;
|
||||
if (!suite) throw new Error("Eval suite is required.");
|
||||
return suite;
|
||||
}
|
||||
|
||||
private assertTarget(orgId: string, type: SdlcReviewTargetType, ref: string): void {
|
||||
if (!Object.values(SDLC_REVIEW_TARGET_TYPE).includes(type)) throw new Error("SDLC review target type is invalid.");
|
||||
const targetRef = ref.trim();
|
||||
if (!targetRef) throw new Error("SDLC review target is required.");
|
||||
if (type === SDLC_REVIEW_TARGET_TYPE.WORKSPACE) {
|
||||
const org = this.db.prepare("select id from organizations where id = ?").get(targetRef);
|
||||
if (!org || targetRef !== orgId) throw new Error("SDLC workspace target is invalid.");
|
||||
return;
|
||||
}
|
||||
if (type === SDLC_REVIEW_TARGET_TYPE.MIGRATION) {
|
||||
const report = this.db.prepare("select id from migration_reports where id = ? and org_id = ?").get(targetRef, orgId);
|
||||
if (!report) throw new Error("SDLC migration target was not found.");
|
||||
return;
|
||||
}
|
||||
const policy = this.db.prepare("select id from policy_rules where id = ? and org_id = ?").get(targetRef, orgId);
|
||||
if (!policy) throw new Error("SDLC policy target was not found.");
|
||||
}
|
||||
|
||||
private reviewChecks(orgId: string, agentKey: SdlcReviewAgentKey, targetType: SdlcReviewTargetType): GateCheck[] {
|
||||
if (agentKey === SDLC_REVIEW_AGENT_KEY.MIGRATION_REVIEWER) return this.migrationReviewChecks(orgId);
|
||||
if (agentKey === SDLC_REVIEW_AGENT_KEY.POLICY_REVIEWER) return this.policyReviewChecks(orgId);
|
||||
return this.codeReviewChecks(orgId, targetType);
|
||||
}
|
||||
|
||||
private codeReviewChecks(orgId: string, targetType: SdlcReviewTargetType): GateCheck[] {
|
||||
const enabledGates = count(this.db, "select count(*) as count from pr_bot_gates where org_id = ? and enabled = 1", orgId);
|
||||
const auditEvents = count(this.db, "select count(*) as count from audit_events where org_id = ?", orgId);
|
||||
return [
|
||||
{ key: "target_type", passed: targetType === SDLC_REVIEW_TARGET_TYPE.WORKSPACE, value: targetType },
|
||||
{ key: "enabled_pr_gates", passed: enabledGates >= 3, value: enabledGates },
|
||||
{ key: "recent_audit_events", passed: auditEvents > 0, value: auditEvents },
|
||||
];
|
||||
}
|
||||
|
||||
private migrationReviewChecks(orgId: string): GateCheck[] {
|
||||
const reports = this.db.prepare("select report_json from migration_reports where org_id = ?").all(orgId) as Array<{ report_json: string }>;
|
||||
const rollbackPacks = reports.filter((report) => hasObjectField(report.report_json, "rollbackPack")).length;
|
||||
const migrationGateEnabled = this.isGateEnabled(orgId, PR_BOT_GATE_KEY.MIGRATION_DIFF);
|
||||
return [
|
||||
{ key: "migration_gate_enabled", passed: migrationGateEnabled, value: migrationGateEnabled },
|
||||
{ key: "rollback_pack_coverage", passed: reports.length === rollbackPacks, value: `${rollbackPacks}/${reports.length}` },
|
||||
];
|
||||
}
|
||||
|
||||
private policyReviewChecks(orgId: string): GateCheck[] {
|
||||
const providerConflicts = count(this.db, "select count(*) as count from providers where org_id = ? and data_policy = 'zero_data_retention' and background_enabled = 1", orgId);
|
||||
const routeRules = count(this.db, "select count(*) as count from approval_routing_rules where org_id = ?", orgId);
|
||||
const policyGateEnabled = this.isGateEnabled(orgId, PR_BOT_GATE_KEY.POLICY_DIFF);
|
||||
return [
|
||||
{ key: "policy_gate_enabled", passed: policyGateEnabled, value: policyGateEnabled },
|
||||
{ key: "provider_policy_conflicts", passed: providerConflicts === 0, value: providerConflicts },
|
||||
{ key: "approval_routes", passed: routeRules > 0, value: routeRules },
|
||||
];
|
||||
}
|
||||
|
||||
private evalChecks(orgId: string, suiteKey: EvalSuiteKey): GateCheck[] {
|
||||
if (suiteKey === EVAL_SUITE_KEY.CONTRACT) return this.contractEvalChecks();
|
||||
if (suiteKey === EVAL_SUITE_KEY.INTEGRATION) return this.integrationEvalChecks(orgId);
|
||||
return this.replayEvalChecks(orgId);
|
||||
}
|
||||
|
||||
private contractEvalChecks(): GateCheck[] {
|
||||
const catalogKeys = PROVIDER_CATALOG.map((provider) => provider.key);
|
||||
const adapterKeys = Object.keys(providerAdapters);
|
||||
const missing = catalogKeys.filter((key) => !adapterKeys.includes(key));
|
||||
const schemaVersion = count(this.db, "select coalesce(max(version), 0) as count from schema_migrations");
|
||||
return [
|
||||
{ key: "provider_adapter_coverage", passed: missing.length === 0, value: `${adapterKeys.length}/${catalogKeys.length}` },
|
||||
{ key: "schema_version", passed: schemaVersion >= 18, value: schemaVersion },
|
||||
];
|
||||
}
|
||||
|
||||
private integrationEvalChecks(orgId: string): GateCheck[] {
|
||||
const chatSpaces = count(this.db, "select count(*) as count from chat_spaces where org_id = ?", orgId);
|
||||
const approvalRoutes = count(this.db, "select count(*) as count from approval_routing_rules where org_id = ?", orgId);
|
||||
const skillSnapshots = count(this.db, `
|
||||
select count(*) as count
|
||||
from skill_runtime_snapshots
|
||||
join agents on agents.id = skill_runtime_snapshots.agent_id
|
||||
where agents.org_id = ?
|
||||
`, orgId);
|
||||
return [
|
||||
{ key: "chat_spaces", passed: chatSpaces > 0, value: chatSpaces },
|
||||
{ key: "approval_routes", passed: approvalRoutes > 0, value: approvalRoutes },
|
||||
{ key: "skill_runtime_snapshots", passed: skillSnapshots > 0, value: skillSnapshots },
|
||||
];
|
||||
}
|
||||
|
||||
private replayEvalChecks(orgId: string): GateCheck[] {
|
||||
const auditEvents = count(this.db, "select count(*) as count from audit_events where org_id = ?", orgId);
|
||||
const operationalEvents = count(this.db, "select count(*) as count from operational_events where org_id = ?", orgId);
|
||||
return [
|
||||
{ key: "audit_events", passed: auditEvents > 0, value: auditEvents },
|
||||
{ key: "operational_events", passed: operationalEvents > 0, value: operationalEvents },
|
||||
];
|
||||
}
|
||||
|
||||
private isGateEnabled(orgId: string, key: string): boolean {
|
||||
const row = this.db.prepare("select enabled from pr_bot_gates where org_id = ? and key = ?")
|
||||
.get(orgId, key) as { enabled: number } | undefined;
|
||||
if (!row) throw new Error("PR bot gate is required.");
|
||||
return row.enabled === 1;
|
||||
}
|
||||
}
|
||||
|
||||
function count(db: DatabaseSync, sql: string, ...params: string[]): number {
|
||||
return (db.prepare(sql).get(...params) as { count: number }).count;
|
||||
}
|
||||
|
||||
function hasObjectField(json: string, field: string): boolean {
|
||||
const parsed = JSON.parse(json) as Record<string, unknown>;
|
||||
return typeof parsed[field] === "object" && parsed[field] !== null;
|
||||
}
|
||||
@@ -59,6 +59,10 @@ export class SnapshotService {
|
||||
.map((event) => ({ ...event, success: toBool(event.success) }));
|
||||
const chaosDrills = list<AppSnapshot["chaosDrills"][number]>(this.db, "chaos_drills", "key asc")
|
||||
.map((drill) => ({ ...drill, enabled: toBool(drill.enabled) }));
|
||||
const sdlcReviewAgents = list<AppSnapshot["sdlcReviewAgents"][number]>(this.db, "sdlc_review_agents", "key asc")
|
||||
.map((agent) => ({ ...agent, enabled: toBool(agent.enabled) }));
|
||||
const prBotGates = list<AppSnapshot["prBotGates"][number]>(this.db, "pr_bot_gates", "key asc")
|
||||
.map((gate) => ({ ...gate, enabled: toBool(gate.enabled) }));
|
||||
|
||||
return {
|
||||
activeOrgId,
|
||||
@@ -111,6 +115,12 @@ export class SnapshotService {
|
||||
operationalEvents,
|
||||
chaosDrills,
|
||||
chaosDrillRuns: list(this.db, "chaos_drill_runs", "started_at desc"),
|
||||
sdlcReviewAgents,
|
||||
sdlcReviewRuns: list(this.db, "sdlc_review_runs", "created_at desc"),
|
||||
evalSuites: list(this.db, "eval_suites", "key asc"),
|
||||
evalCases: list(this.db, "eval_cases", "title asc"),
|
||||
evalRuns: list(this.db, "eval_runs", "created_at desc"),
|
||||
prBotGates,
|
||||
auditEvents: list<AppSnapshot["auditEvents"][number]>(this.db, "audit_events", "created_at desc").slice(0, 200),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -40,6 +40,9 @@ const api: DesktopApi = {
|
||||
createMigrationReport: (input) => invoke(IPC_CHANNELS.createMigrationReport, input),
|
||||
setFeatureFlag: (input) => invoke(IPC_CHANNELS.setFeatureFlag, input),
|
||||
runChaosDrill: (input) => invoke(IPC_CHANNELS.runChaosDrill, input),
|
||||
runSdlcReview: (input) => invoke(IPC_CHANNELS.runSdlcReview, input),
|
||||
runEvalSuite: (input) => invoke(IPC_CHANNELS.runEvalSuite, input),
|
||||
setPrBotGate: (input) => invoke(IPC_CHANNELS.setPrBotGate, input),
|
||||
createComplianceExport: (input) => invoke(IPC_CHANNELS.createComplianceExport, input),
|
||||
createLegalHold: (input) => invoke(IPC_CHANNELS.createLegalHold, input),
|
||||
setLegalHoldState: (input) => invoke(IPC_CHANNELS.setLegalHoldState, input),
|
||||
|
||||
@@ -7,6 +7,7 @@ import { BillingPanel } from "./settings/BillingPanel";
|
||||
import { ModelsPanel } from "./settings/ModelsPanel";
|
||||
import { PoliciesPanel } from "./settings/PoliciesPanel";
|
||||
import { OperationsPanel } from "./settings/OperationsPanel";
|
||||
import { SdlcPanel } from "./settings/SdlcPanel";
|
||||
import { MigrationPanel } from "./settings/MigrationPanel";
|
||||
import { CompliancePanel } from "./settings/CompliancePanel";
|
||||
import { NotificationsPanel, ProfilePanel, SecurityPanel } from "./settings/AccountPanels";
|
||||
@@ -22,6 +23,7 @@ type SettingsTab =
|
||||
| "skills"
|
||||
| "policies"
|
||||
| "operations"
|
||||
| "sdlc"
|
||||
| "compliance"
|
||||
| "migration"
|
||||
| "profile"
|
||||
@@ -44,6 +46,7 @@ const AGENTS_GROUP: NavSpec[] = [
|
||||
{ id: "skills", label: "Skills" },
|
||||
{ id: "policies", label: "Policies" },
|
||||
{ id: "operations", label: "Operations" },
|
||||
{ id: "sdlc", label: "SDLC" },
|
||||
{ id: "compliance", label: "Compliance" },
|
||||
{ id: "migration", label: "Migration" },
|
||||
];
|
||||
@@ -91,6 +94,12 @@ export function SettingsPage({ snapshot, actions }: PageProps) {
|
||||
const operationalEvents = useMemo(() => snapshot.operationalEvents.filter((item) => item.orgId === orgId), [snapshot.operationalEvents, orgId]);
|
||||
const chaosDrills = useMemo(() => snapshot.chaosDrills.filter((item) => item.orgId === orgId), [snapshot.chaosDrills, orgId]);
|
||||
const chaosDrillRuns = useMemo(() => snapshot.chaosDrillRuns.filter((item) => item.orgId === orgId), [snapshot.chaosDrillRuns, orgId]);
|
||||
const sdlcReviewAgents = useMemo(() => snapshot.sdlcReviewAgents.filter((item) => item.orgId === orgId), [snapshot.sdlcReviewAgents, orgId]);
|
||||
const sdlcReviewRuns = useMemo(() => snapshot.sdlcReviewRuns.filter((item) => item.orgId === orgId), [snapshot.sdlcReviewRuns, orgId]);
|
||||
const evalSuites = useMemo(() => snapshot.evalSuites.filter((item) => item.orgId === orgId), [snapshot.evalSuites, orgId]);
|
||||
const evalCases = useMemo(() => snapshot.evalCases.filter((item) => item.orgId === orgId), [snapshot.evalCases, orgId]);
|
||||
const evalRuns = useMemo(() => snapshot.evalRuns.filter((item) => item.orgId === orgId), [snapshot.evalRuns, orgId]);
|
||||
const prBotGates = useMemo(() => snapshot.prBotGates.filter((item) => item.orgId === orgId), [snapshot.prBotGates, orgId]);
|
||||
|
||||
return (
|
||||
<div className="split-main" style={{ height: "calc(100vh - 44px)", margin: "-28px -32px -40px" }}>
|
||||
@@ -158,6 +167,20 @@ export function SettingsPage({ snapshot, actions }: PageProps) {
|
||||
actions={actions}
|
||||
/>
|
||||
)}
|
||||
{tab === "sdlc" && (
|
||||
<SdlcPanel
|
||||
orgId={orgId}
|
||||
reviewAgents={sdlcReviewAgents}
|
||||
reviewRuns={sdlcReviewRuns}
|
||||
evalSuites={evalSuites}
|
||||
evalCases={evalCases}
|
||||
evalRuns={evalRuns}
|
||||
prBotGates={prBotGates}
|
||||
migrationReports={migrationReports}
|
||||
policies={policies}
|
||||
actions={actions}
|
||||
/>
|
||||
)}
|
||||
{tab === "compliance" && (
|
||||
<CompliancePanel
|
||||
orgId={orgId}
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
import { Button, Pill } from "../../shared/ui";
|
||||
import type {
|
||||
EvalCaseRecord,
|
||||
EvalRunRecord,
|
||||
EvalSuiteRecord,
|
||||
MigrationReportRecord,
|
||||
PolicyRuleRecord,
|
||||
PrBotGateRecord,
|
||||
SdlcReviewAgentRecord,
|
||||
SdlcReviewRunRecord,
|
||||
SdlcReviewTargetType,
|
||||
} from "../../../../shared/domain";
|
||||
import { SDLC_REVIEW_AGENT_KEY, SDLC_REVIEW_TARGET_TYPE } from "../../../../shared/domain";
|
||||
import type { SnapshotActions } from "../../app/useSnapshot";
|
||||
import { formatRelative } from "../types";
|
||||
|
||||
export function SdlcPanel({
|
||||
orgId,
|
||||
reviewAgents,
|
||||
reviewRuns,
|
||||
evalSuites,
|
||||
evalCases,
|
||||
evalRuns,
|
||||
prBotGates,
|
||||
migrationReports,
|
||||
policies,
|
||||
actions,
|
||||
}: {
|
||||
orgId: string;
|
||||
reviewAgents: SdlcReviewAgentRecord[];
|
||||
reviewRuns: SdlcReviewRunRecord[];
|
||||
evalSuites: EvalSuiteRecord[];
|
||||
evalCases: EvalCaseRecord[];
|
||||
evalRuns: EvalRunRecord[];
|
||||
prBotGates: PrBotGateRecord[];
|
||||
migrationReports: MigrationReportRecord[];
|
||||
policies: PolicyRuleRecord[];
|
||||
actions: SnapshotActions;
|
||||
}) {
|
||||
return (
|
||||
<div className="page">
|
||||
<h1 className="page-h1">SDLC</h1>
|
||||
<p className="page-sub">{reviewAgents.length} agents / {evalSuites.length} suites / {prBotGates.length} gates.</p>
|
||||
<ReviewAgentsTable
|
||||
orgId={orgId}
|
||||
agents={reviewAgents}
|
||||
runs={reviewRuns}
|
||||
migrationReports={migrationReports}
|
||||
policies={policies}
|
||||
actions={actions}
|
||||
/>
|
||||
<EvalSuitesTable orgId={orgId} suites={evalSuites} cases={evalCases} runs={evalRuns} actions={actions} />
|
||||
<PrBotGatesTable orgId={orgId} gates={prBotGates} actions={actions} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ReviewAgentsTable({
|
||||
orgId,
|
||||
agents,
|
||||
runs,
|
||||
migrationReports,
|
||||
policies,
|
||||
actions,
|
||||
}: {
|
||||
orgId: string;
|
||||
agents: SdlcReviewAgentRecord[];
|
||||
runs: SdlcReviewRunRecord[];
|
||||
migrationReports: MigrationReportRecord[];
|
||||
policies: PolicyRuleRecord[];
|
||||
actions: SnapshotActions;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<div className="section-label" style={{ marginBottom: 10 }}>Review agents</div>
|
||||
<div className="card" style={{ padding: 0, overflow: "hidden", marginBottom: 24 }}>
|
||||
<div className="table-row head" style={{ gridTemplateColumns: "minmax(0, 1.4fr) minmax(0, 1fr) 96px 82px" }}>
|
||||
<div>Agent</div><div>Target</div><div>Last run</div><div>Action</div>
|
||||
</div>
|
||||
{agents.map((agent, index) => {
|
||||
const target = resolveReviewTarget(agent, orgId, migrationReports, policies);
|
||||
const run = runs.find((item) => item.reviewAgentId === agent.id) ?? null;
|
||||
return (
|
||||
<div
|
||||
key={agent.id}
|
||||
className="table-row"
|
||||
style={{
|
||||
gridTemplateColumns: "minmax(0, 1.4fr) minmax(0, 1fr) 96px 82px",
|
||||
borderBottom: index === agents.length - 1 ? "none" : "1px solid var(--border-soft)",
|
||||
}}
|
||||
>
|
||||
<div style={{ minWidth: 0 }}>
|
||||
<div style={{ fontWeight: 540, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{agent.label}</div>
|
||||
<div className="mono" style={{ fontSize: 11, color: "var(--text-3)" }}>{agent.specialty}</div>
|
||||
</div>
|
||||
<div><Pill>{target.label}</Pill></div>
|
||||
<div className="mono" style={{ fontSize: 12, color: "var(--text-3)" }}>{run ? `${run.state} · ${formatRelative(run.createdAt)}` : "0 runs"}</div>
|
||||
<Button
|
||||
size="small"
|
||||
disabled={!agent.enabled || !target.ready}
|
||||
onClick={() => void actions.run(() => window.agentCompany.runSdlcReview({
|
||||
orgId,
|
||||
reviewAgentId: agent.id,
|
||||
targetType: target.type,
|
||||
targetRef: target.ref,
|
||||
}))}
|
||||
>
|
||||
Run
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function EvalSuitesTable({
|
||||
orgId,
|
||||
suites,
|
||||
cases,
|
||||
runs,
|
||||
actions,
|
||||
}: {
|
||||
orgId: string;
|
||||
suites: EvalSuiteRecord[];
|
||||
cases: EvalCaseRecord[];
|
||||
runs: EvalRunRecord[];
|
||||
actions: SnapshotActions;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<div className="section-label" style={{ marginBottom: 10 }}>Eval suites</div>
|
||||
<div className="card" style={{ padding: 0, overflow: "hidden", marginBottom: 24 }}>
|
||||
<div className="table-row head" style={{ gridTemplateColumns: "minmax(0, 1.4fr) 90px 96px 82px" }}>
|
||||
<div>Suite</div><div>Cases</div><div>Last run</div><div>Action</div>
|
||||
</div>
|
||||
{suites.map((suite, index) => {
|
||||
const run = runs.find((item) => item.suiteId === suite.id) ?? null;
|
||||
const caseCount = cases.filter((item) => item.suiteId === suite.id).length;
|
||||
return (
|
||||
<div
|
||||
key={suite.id}
|
||||
className="table-row"
|
||||
style={{
|
||||
gridTemplateColumns: "minmax(0, 1.4fr) 90px 96px 82px",
|
||||
borderBottom: index === suites.length - 1 ? "none" : "1px solid var(--border-soft)",
|
||||
}}
|
||||
>
|
||||
<div style={{ minWidth: 0 }}>
|
||||
<div style={{ fontWeight: 540, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{suite.label}</div>
|
||||
<div className="mono" style={{ fontSize: 11, color: "var(--text-3)" }}>{suite.scope}</div>
|
||||
</div>
|
||||
<div className="num mono">{caseCount}</div>
|
||||
<div className="mono" style={{ fontSize: 12, color: "var(--text-3)" }}>{run ? `${run.state} · ${formatRelative(run.createdAt)}` : "0 runs"}</div>
|
||||
<Button size="small" onClick={() => void actions.run(() => window.agentCompany.runEvalSuite({ orgId, suiteId: suite.id }))}>
|
||||
Run
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function PrBotGatesTable({
|
||||
orgId,
|
||||
gates,
|
||||
actions,
|
||||
}: {
|
||||
orgId: string;
|
||||
gates: PrBotGateRecord[];
|
||||
actions: SnapshotActions;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<div className="section-label" style={{ marginBottom: 10 }}>PR bot gates</div>
|
||||
<div className="card" style={{ padding: 0, overflow: "hidden" }}>
|
||||
<div className="table-row head" style={{ gridTemplateColumns: "minmax(0, 1.4fr) 112px minmax(0, 1fr)" }}>
|
||||
<div>Gate</div><div>State</div><div>Updated</div>
|
||||
</div>
|
||||
{gates.map((gate, index) => (
|
||||
<div
|
||||
key={gate.id}
|
||||
className="table-row"
|
||||
style={{
|
||||
gridTemplateColumns: "minmax(0, 1.4fr) 112px minmax(0, 1fr)",
|
||||
borderBottom: index === gates.length - 1 ? "none" : "1px solid var(--border-soft)",
|
||||
}}
|
||||
>
|
||||
<div style={{ minWidth: 0 }}>
|
||||
<div style={{ fontWeight: 540, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{gate.label}</div>
|
||||
<div className="mono" style={{ fontSize: 11, color: "var(--text-3)" }}>{gate.gateType}</div>
|
||||
</div>
|
||||
<label className="row" style={{ gap: 8, fontSize: 12, color: "var(--text-2)" }}>
|
||||
<input
|
||||
aria-label={`${gate.label} enabled`}
|
||||
type="checkbox"
|
||||
checked={gate.enabled}
|
||||
onChange={(event) => {
|
||||
void actions.run(() => window.agentCompany.setPrBotGate({ orgId, gateId: gate.id, enabled: event.target.checked }));
|
||||
}}
|
||||
/>
|
||||
{gate.enabled ? "On" : "Off"}
|
||||
</label>
|
||||
<div className="mono" style={{ fontSize: 12, color: "var(--text-3)" }}>{formatRelative(gate.updatedAt)}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function resolveReviewTarget(
|
||||
agent: SdlcReviewAgentRecord,
|
||||
orgId: string,
|
||||
migrationReports: MigrationReportRecord[],
|
||||
policies: PolicyRuleRecord[],
|
||||
): { type: SdlcReviewTargetType; ref: string; label: string; ready: boolean } {
|
||||
if (agent.key === SDLC_REVIEW_AGENT_KEY.MIGRATION_REVIEWER) {
|
||||
const report = migrationReports[0] ?? null;
|
||||
return {
|
||||
type: SDLC_REVIEW_TARGET_TYPE.MIGRATION,
|
||||
ref: report?.id ?? orgId,
|
||||
label: report ? "migration" : "migration report",
|
||||
ready: Boolean(report),
|
||||
};
|
||||
}
|
||||
if (agent.key === SDLC_REVIEW_AGENT_KEY.POLICY_REVIEWER) {
|
||||
const policy = policies[0] ?? null;
|
||||
return {
|
||||
type: SDLC_REVIEW_TARGET_TYPE.POLICY,
|
||||
ref: policy?.id ?? orgId,
|
||||
label: policy ? "policy" : "policy rule",
|
||||
ready: Boolean(policy),
|
||||
};
|
||||
}
|
||||
return { type: SDLC_REVIEW_TARGET_TYPE.WORKSPACE, ref: orgId, label: "workspace", ready: true };
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import type {
|
||||
RiskLevel,
|
||||
RoleKey,
|
||||
SearchEntityType,
|
||||
SdlcReviewTargetType,
|
||||
SocialPlatform,
|
||||
TaskPriority,
|
||||
TaskStatus,
|
||||
@@ -288,6 +289,24 @@ export interface RunChaosDrillInput {
|
||||
drillId: string;
|
||||
}
|
||||
|
||||
export interface RunSdlcReviewInput {
|
||||
orgId: string;
|
||||
reviewAgentId: string;
|
||||
targetType: SdlcReviewTargetType;
|
||||
targetRef: string;
|
||||
}
|
||||
|
||||
export interface RunEvalSuiteInput {
|
||||
orgId: string;
|
||||
suiteId: string;
|
||||
}
|
||||
|
||||
export interface SetPrBotGateInput {
|
||||
orgId: string;
|
||||
gateId: string;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export interface BootstrapOnboardingInput {
|
||||
companyName: string;
|
||||
companyDescription: string;
|
||||
|
||||
@@ -27,6 +27,8 @@ import type {
|
||||
LaunchSocialLoginInput,
|
||||
ResolveAgentSkillsInput,
|
||||
RunChaosDrillInput,
|
||||
RunEvalSuiteInput,
|
||||
RunSdlcReviewInput,
|
||||
SaveProviderCredentialInput,
|
||||
SearchInput,
|
||||
SendMessageInput,
|
||||
@@ -34,6 +36,7 @@ import type {
|
||||
SetFilePinnedInput,
|
||||
SetFeatureFlagInput,
|
||||
SetLegalHoldStateInput,
|
||||
SetPrBotGateInput,
|
||||
SetRetentionFreezeStateInput,
|
||||
SetScheduledPostStateInput,
|
||||
SyncEmployeeIdentityInput,
|
||||
@@ -79,6 +82,9 @@ export interface DesktopApi {
|
||||
createMigrationReport(input: CreateMigrationReportInput): Promise<DesktopResult<AppSnapshot>>;
|
||||
setFeatureFlag(input: SetFeatureFlagInput): Promise<DesktopResult<AppSnapshot>>;
|
||||
runChaosDrill(input: RunChaosDrillInput): Promise<DesktopResult<AppSnapshot>>;
|
||||
runSdlcReview(input: RunSdlcReviewInput): Promise<DesktopResult<AppSnapshot>>;
|
||||
runEvalSuite(input: RunEvalSuiteInput): Promise<DesktopResult<AppSnapshot>>;
|
||||
setPrBotGate(input: SetPrBotGateInput): Promise<DesktopResult<AppSnapshot>>;
|
||||
createComplianceExport(input: CreateComplianceExportInput): Promise<DesktopResult<AppSnapshot>>;
|
||||
createLegalHold(input: CreateLegalHoldInput): Promise<DesktopResult<AppSnapshot>>;
|
||||
setLegalHoldState(input: SetLegalHoldStateInput): Promise<DesktopResult<AppSnapshot>>;
|
||||
|
||||
@@ -4,12 +4,14 @@ import type { AccessDecisionRecord, EmployeeIdentityRecord, RoleGrantRecord, Ser
|
||||
import type { ApprovalAssignmentRecord, ApprovalRoutingRuleRecord, HiringPolicyRecord, OrgDepartmentRecord } from "./org-governance-domain";
|
||||
import type { ChaosDrillRecord, ChaosDrillRunRecord, FeatureFlagRecord, OperationalEventRecord, SloTargetRecord } from "./operations-domain";
|
||||
import type { ProviderDataPolicy, ProviderExecutionMode } from "./provider-policy-domain";
|
||||
import type { EvalCaseRecord, EvalRunRecord, EvalSuiteRecord, PrBotGateRecord, SdlcReviewAgentRecord, SdlcReviewRunRecord } from "./sdlc-domain";
|
||||
export { BOARD_INTERVENTION_ACTION, BOARD_INTERVENTION_MODE, CHAT_THREAD_BOARD_STATE, type BoardInterventionAction, type BoardInterventionMode, type BoardInterventionRecord, type ChatThreadBoardState } from "./board-domain";
|
||||
export { COMPLIANCE_EXPORT_STATE, COMPLIANCE_EXPORT_TYPE, type ComplianceExportRecord, type ComplianceExportState, type ComplianceExportType, type ComplianceSubjectType, type LegalHoldEntityType, type LegalHoldRecord, type RetentionFreezeRecord, type RetentionFreezeScopeType } from "./compliance-domain";
|
||||
export { ACCESS_DECISION, IDENTITY_SOURCE, ROLE_KEY, type AccessDecision, type AccessDecisionRecord, type EmployeeIdentityRecord, type IdentitySource, type PrincipalType, type RoleGrantRecord, type RoleKey, type ServiceIdentityRecord } from "./identity-domain";
|
||||
export { ROUTE_TO_TYPE, type ApprovalAssignmentRecord, type ApprovalRoutingRuleRecord, type HiringPolicyRecord, type OrgDepartmentRecord, type RouteToType } from "./org-governance-domain";
|
||||
export { CHAOS_DRILL_SCENARIO, CHAOS_DRILL_STATE, FEATURE_FLAG_KEY, SLO_SIGNAL_KEY, type ChaosDrillRecord, type ChaosDrillRunRecord, type ChaosDrillScenario, type ChaosDrillState, type FeatureFlagKey, type FeatureFlagRecord, type OperationalEventRecord, type SloSignalKey, type SloTargetRecord } from "./operations-domain";
|
||||
export { PROVIDER_DATA_POLICY, PROVIDER_EXECUTION_MODE, type ProviderDataPolicy, type ProviderExecutionMode } from "./provider-policy-domain";
|
||||
export { EVAL_SUITE_KEY, PR_BOT_GATE_KEY, SDLC_REVIEW_AGENT_KEY, SDLC_REVIEW_TARGET_TYPE, SDLC_RUN_STATE, type EvalCaseRecord, type EvalRunRecord, type EvalSuiteKey, type EvalSuiteRecord, type PrBotGateKey, type PrBotGateRecord, type SdlcReviewAgentKey, type SdlcReviewAgentRecord, type SdlcReviewRunRecord, type SdlcReviewTargetType, type SdlcRunState } from "./sdlc-domain";
|
||||
export { SEARCH_ENTITY_TYPES, type SearchEntityType, type SearchResultRecord } from "./search-domain";
|
||||
export const TASK_STATUS = { BACKLOG: "backlog", IN_PROGRESS: "in_progress", REVIEW: "review", DONE: "done" } as const;
|
||||
export const APPROVAL_STATE = { PENDING: "pending", APPROVED: "approved", DECLINED: "declined" } as const;
|
||||
@@ -474,5 +476,6 @@ export interface AppSnapshot {
|
||||
employeeIdentities: EmployeeIdentityRecord[]; serviceIdentities: ServiceIdentityRecord[]; roleGrants: RoleGrantRecord[]; accessDecisions: AccessDecisionRecord[];
|
||||
orgDepartments: OrgDepartmentRecord[]; hiringPolicies: HiringPolicyRecord[]; approvalRoutingRules: ApprovalRoutingRuleRecord[]; approvalAssignments: ApprovalAssignmentRecord[];
|
||||
featureFlags: FeatureFlagRecord[]; sloTargets: SloTargetRecord[]; operationalEvents: OperationalEventRecord[]; chaosDrills: ChaosDrillRecord[]; chaosDrillRuns: ChaosDrillRunRecord[];
|
||||
sdlcReviewAgents: SdlcReviewAgentRecord[]; sdlcReviewRuns: SdlcReviewRunRecord[]; evalSuites: EvalSuiteRecord[]; evalCases: EvalCaseRecord[]; evalRuns: EvalRunRecord[]; prBotGates: PrBotGateRecord[];
|
||||
auditEvents: AuditEventRecord[];
|
||||
}
|
||||
@@ -32,6 +32,9 @@ export const IPC_CHANNELS = {
|
||||
createMigrationReport: "agentcompany:create-migration-report",
|
||||
setFeatureFlag: "agentcompany:set-feature-flag",
|
||||
runChaosDrill: "agentcompany:run-chaos-drill",
|
||||
runSdlcReview: "agentcompany:run-sdlc-review",
|
||||
runEvalSuite: "agentcompany:run-eval-suite",
|
||||
setPrBotGate: "agentcompany:set-pr-bot-gate",
|
||||
createComplianceExport: "agentcompany:create-compliance-export",
|
||||
createLegalHold: "agentcompany:create-legal-hold",
|
||||
setLegalHoldState: "agentcompany:set-legal-hold-state",
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
type ValueOf<T> = T[keyof T];
|
||||
|
||||
export const SDLC_REVIEW_AGENT_KEY = {
|
||||
CODE_REVIEWER: "code_reviewer",
|
||||
MIGRATION_REVIEWER: "migration_reviewer",
|
||||
POLICY_REVIEWER: "policy_reviewer",
|
||||
} as const;
|
||||
|
||||
export const SDLC_REVIEW_TARGET_TYPE = { WORKSPACE: "workspace", MIGRATION: "migration", POLICY: "policy" } as const;
|
||||
export const EVAL_SUITE_KEY = { CONTRACT: "contract", INTEGRATION: "integration", REPLAY: "replay" } as const;
|
||||
export const SDLC_RUN_STATE = { PASSED: "passed", FAILED: "failed" } as const;
|
||||
export const PR_BOT_GATE_KEY = {
|
||||
STATIC_CHECKS: "static_checks",
|
||||
MIGRATION_DIFF: "migration_diff",
|
||||
POLICY_DIFF: "policy_diff",
|
||||
HUMAN_APPROVAL: "human_approval",
|
||||
} as const;
|
||||
|
||||
export type SdlcReviewAgentKey = ValueOf<typeof SDLC_REVIEW_AGENT_KEY>;
|
||||
export type SdlcReviewTargetType = ValueOf<typeof SDLC_REVIEW_TARGET_TYPE>;
|
||||
export type EvalSuiteKey = ValueOf<typeof EVAL_SUITE_KEY>;
|
||||
export type SdlcRunState = ValueOf<typeof SDLC_RUN_STATE>;
|
||||
export type PrBotGateKey = ValueOf<typeof PR_BOT_GATE_KEY>;
|
||||
|
||||
export interface SdlcReviewAgentRecord {
|
||||
id: string; orgId: string; key: SdlcReviewAgentKey; label: string; specialty: string; enabled: boolean; createdAt: string; updatedAt: string;
|
||||
}
|
||||
|
||||
export interface SdlcReviewRunRecord {
|
||||
id: string; orgId: string; reviewAgentId: string; targetType: SdlcReviewTargetType; targetRef: string;
|
||||
state: SdlcRunState; resultJson: string; createdAt: string; completedAt: string;
|
||||
}
|
||||
|
||||
export interface EvalSuiteRecord {
|
||||
id: string; orgId: string; key: EvalSuiteKey; label: string; scope: string; createdAt: string; updatedAt: string;
|
||||
}
|
||||
|
||||
export interface EvalCaseRecord {
|
||||
id: string; orgId: string; suiteId: string; title: string; sourceType: string; sourceRef: string; expectedSignal: string; createdAt: string;
|
||||
}
|
||||
|
||||
export interface EvalRunRecord {
|
||||
id: string; orgId: string; suiteId: string; state: SdlcRunState; passedCount: number; failedCount: number; resultJson: string; createdAt: string; completedAt: string;
|
||||
}
|
||||
|
||||
export interface PrBotGateRecord {
|
||||
id: string; orgId: string; key: PrBotGateKey; label: string; gateType: string; enabled: boolean; createdAt: string; updatedAt: string;
|
||||
}
|
||||
@@ -53,6 +53,14 @@ test("onboards, hires, and creates work in the Electron shell", async () => {
|
||||
await expect(page.getByText("Chaos drills").first()).toBeVisible();
|
||||
await page.getByRole("button", { name: "Run" }).first().click();
|
||||
await expect(page.getByText("passed").first()).toBeVisible();
|
||||
await page.getByRole("button", { name: "SDLC", exact: true }).click();
|
||||
await expect(page.getByText("Review agents", { exact: true })).toBeVisible();
|
||||
await expect(page.getByText("Eval suites", { exact: true })).toBeVisible();
|
||||
await expect(page.getByText("PR bot gates", { exact: true })).toBeVisible();
|
||||
await page.getByRole("button", { name: "Run" }).first().click();
|
||||
await expect(page.getByText("passed").first()).toBeVisible();
|
||||
await page.getByRole("button", { name: "Run" }).nth(3).click();
|
||||
await expect(page.getByText(/passed ·/).first()).toBeVisible();
|
||||
|
||||
await page.getByRole("button", { name: "Work", exact: true }).click();
|
||||
await page.getByRole("button", { name: "New task" }).click();
|
||||
@@ -134,7 +142,7 @@ async function verifyPagesDoNotOverflow(page: Page): Promise<void> {
|
||||
await page.getByRole("button", { name: nav, exact: true }).click();
|
||||
await expectPageToFit(page);
|
||||
}
|
||||
for (const tab of ["Workspace", "Members", "Billing", "Models", "Providers", "Skills", "Policies", "Operations", "Compliance", "Migration", "Profile", "Notifications", "Security"]) {
|
||||
for (const tab of ["Workspace", "Members", "Billing", "Models", "Providers", "Skills", "Policies", "Operations", "SDLC", "Compliance", "Migration", "Profile", "Notifications", "Security"]) {
|
||||
await page.getByRole("button", { name: "Settings" }).click();
|
||||
await page.getByRole("button", { name: tab, exact: true }).click();
|
||||
await expectPageToFit(page);
|
||||
|
||||
@@ -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]);
|
||||
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]);
|
||||
connection.close();
|
||||
});
|
||||
|
||||
@@ -51,6 +51,9 @@ describe("domain services", () => {
|
||||
expect(state.featureFlags.map((flag) => flag.key)).toContain("chaos_drills");
|
||||
expect(state.sloTargets.map((target) => target.signalKey)).toContain("provider_request_success");
|
||||
expect(state.chaosDrills.map((drill) => drill.scenarioKey)).toContain("approval_backlog_gate");
|
||||
expect(state.sdlcReviewAgents.map((agent) => agent.key)).toContain("code_reviewer");
|
||||
expect(state.evalSuites.map((suite) => suite.key)).toContain("contract");
|
||||
expect(state.prBotGates.map((gate) => gate.key)).toContain("static_checks");
|
||||
expect(state.chatSpaces.map((space) => space.name)).toContain("board");
|
||||
expect(state.policyRules[0]?.action).toBe("task_create");
|
||||
expect(state.auditEvents[0]?.action).toBe("created");
|
||||
|
||||
@@ -113,6 +113,12 @@ describe("migration service", () => {
|
||||
create table operational_events(id text primary key, org_id text);
|
||||
create table chaos_drills(id text primary key, org_id text);
|
||||
create table chaos_drill_runs(id text primary key, org_id text, drill_id text);
|
||||
create table sdlc_review_agents(id text primary key, org_id text);
|
||||
create table sdlc_review_runs(id text primary key, org_id text, review_agent_id text);
|
||||
create table eval_suites(id text primary key, org_id text);
|
||||
create table eval_cases(id text primary key, org_id text, suite_id text);
|
||||
create table eval_runs(id text primary key, org_id text, suite_id text);
|
||||
create table pr_bot_gates(id text primary key, org_id text);
|
||||
create table chat_spaces(id text primary key, org_id text);
|
||||
create table chat_messages(id text primary key, org_id text, space_id text, thread_id text);
|
||||
create table chat_threads(id text primary key, org_id text, space_id text, root_message_id text);
|
||||
@@ -173,6 +179,12 @@ describe("migration service", () => {
|
||||
insert into operational_events(id, org_id) values ('op-1', 'org-1');
|
||||
insert into chaos_drills(id, org_id) values ('drill-1', 'org-1');
|
||||
insert into chaos_drill_runs(id, org_id, drill_id) values ('drill-run-1', 'org-1', 'drill-1');
|
||||
insert into sdlc_review_agents(id, org_id) values ('reviewer-1', 'org-1');
|
||||
insert into sdlc_review_runs(id, org_id, review_agent_id) values ('review-run-1', 'org-1', 'reviewer-1');
|
||||
insert into eval_suites(id, org_id) values ('suite-1', 'org-1');
|
||||
insert into eval_cases(id, org_id, suite_id) values ('case-1', 'org-1', 'suite-1');
|
||||
insert into eval_runs(id, org_id, suite_id) values ('eval-run-1', 'org-1', 'suite-1');
|
||||
insert into pr_bot_gates(id, org_id) values ('gate-1', 'org-1');
|
||||
insert into chat_threads(id, org_id, space_id, root_message_id) values ('thread-1', 'org-1', 'space-1', null);
|
||||
insert into chat_messages(id, org_id, space_id, thread_id) values ('msg-1', 'org-1', 'space-1', 'thread-1');
|
||||
insert into chat_memberships(space_id, principal_type, principal_id) values ('space-1', 'employee', 'emp-1');
|
||||
@@ -219,6 +231,8 @@ describe("migration service", () => {
|
||||
expect(body.tableCounts.hiring_policies).toBe(1);
|
||||
expect(body.tableCounts.feature_flags).toBe(1);
|
||||
expect(body.tableCounts.chaos_drill_runs).toBe(1);
|
||||
expect(body.tableCounts.sdlc_review_runs).toBe(1);
|
||||
expect(body.tableCounts.eval_cases).toBe(1);
|
||||
expect(body.tableCounts.role_grants).toBe(1);
|
||||
expect(body.tableCounts.skill_artifacts).toBe(1);
|
||||
expect(body.tableCounts.chat_read_state).toBe(1);
|
||||
@@ -235,6 +249,8 @@ describe("migration service", () => {
|
||||
expect(body.idMaps.approval_assignments).toEqual(["assign-1"]);
|
||||
expect(body.idMaps.operational_events).toEqual(["op-1"]);
|
||||
expect(body.idMaps.chaos_drill_runs).toEqual(["drill-run-1"]);
|
||||
expect(body.idMaps.sdlc_review_runs).toEqual(["review-run-1"]);
|
||||
expect(body.idMaps.eval_runs).toEqual(["eval-run-1"]);
|
||||
expect(body.idMaps.board_interventions).toEqual(["bdi-1"]);
|
||||
expect(body.idMaps.retention_freezes).toEqual(["freeze-1"]);
|
||||
expect(body.idMaps.provider_usage_windows).toEqual(["usage-1"]);
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { DatabaseConnection } from "../../src/main/database/connection";
|
||||
import { OrganizationService } from "../../src/main/services/organization-service";
|
||||
import { SdlcService } from "../../src/main/services/sdlc-service";
|
||||
import { SnapshotService } from "../../src/main/services/snapshot-service";
|
||||
import { SDLC_REVIEW_TARGET_TYPE } from "../../src/shared/domain";
|
||||
|
||||
function createConnection(): DatabaseConnection {
|
||||
return new DatabaseConnection(join(tmpdir(), `agentcompany-new-sdlc-${randomUUID()}.db`));
|
||||
}
|
||||
|
||||
function createOrganization(organization: OrganizationService): string {
|
||||
return organization.createOrganization({
|
||||
organizationName: "Acme Operations",
|
||||
employeeName: "Dana Lee",
|
||||
mission: "Coordinate agents with auditable approvals.",
|
||||
goal: "Ship a verified control plane.",
|
||||
});
|
||||
}
|
||||
|
||||
describe("SDLC service", () => {
|
||||
test("seeds review agents, eval suites, eval cases, and PR bot gates", () => {
|
||||
const connection = createConnection();
|
||||
const organization = new OrganizationService(connection.db);
|
||||
const snapshot = new SnapshotService(connection.db);
|
||||
|
||||
connection.transaction(() => {
|
||||
createOrganization(organization);
|
||||
});
|
||||
|
||||
const state = snapshot.load();
|
||||
expect(state.sdlcReviewAgents.map((agent) => agent.key).sort()).toEqual(["code_reviewer", "migration_reviewer", "policy_reviewer"]);
|
||||
expect(state.evalSuites.map((suite) => suite.key).sort()).toEqual(["contract", "integration", "replay"]);
|
||||
expect(state.evalCases).toHaveLength(6);
|
||||
expect(state.prBotGates.map((gate) => gate.key).sort()).toEqual(["human_approval", "migration_diff", "policy_diff", "static_checks"]);
|
||||
connection.close();
|
||||
});
|
||||
|
||||
test("runs review agents against validated workspace targets", () => {
|
||||
const connection = createConnection();
|
||||
const organization = new OrganizationService(connection.db);
|
||||
const sdlc = new SdlcService(connection.db);
|
||||
const snapshot = new SnapshotService(connection.db);
|
||||
let orgId = "";
|
||||
let reviewAgentId = "";
|
||||
|
||||
connection.transaction(() => {
|
||||
orgId = createOrganization(organization);
|
||||
reviewAgentId = (connection.db.prepare("select id from sdlc_review_agents where org_id = ? and key = ?")
|
||||
.get(orgId, "code_reviewer") as { id: string }).id;
|
||||
sdlc.runReview({ orgId, reviewAgentId, targetType: SDLC_REVIEW_TARGET_TYPE.WORKSPACE, targetRef: orgId });
|
||||
});
|
||||
|
||||
const state = snapshot.load();
|
||||
expect(state.sdlcReviewRuns[0]).toMatchObject({ orgId, reviewAgentId, targetType: SDLC_REVIEW_TARGET_TYPE.WORKSPACE, state: "passed" });
|
||||
expect(state.auditEvents.some((event) => event.entityType === "sdlc_review_run" && event.action === "created")).toBe(true);
|
||||
connection.close();
|
||||
});
|
||||
|
||||
test("runs contract eval suites and toggles PR bot gates", () => {
|
||||
const connection = createConnection();
|
||||
const organization = new OrganizationService(connection.db);
|
||||
const sdlc = new SdlcService(connection.db);
|
||||
const snapshot = new SnapshotService(connection.db);
|
||||
let orgId = "";
|
||||
let suiteId = "";
|
||||
let gateId = "";
|
||||
|
||||
connection.transaction(() => {
|
||||
orgId = createOrganization(organization);
|
||||
suiteId = (connection.db.prepare("select id from eval_suites where org_id = ? and key = ?")
|
||||
.get(orgId, "contract") as { id: string }).id;
|
||||
gateId = (connection.db.prepare("select id from pr_bot_gates where org_id = ? and key = ?")
|
||||
.get(orgId, "static_checks") as { id: string }).id;
|
||||
sdlc.runEvalSuite({ orgId, suiteId });
|
||||
sdlc.setPrBotGate({ orgId, gateId, enabled: false });
|
||||
});
|
||||
|
||||
const state = snapshot.load();
|
||||
expect(state.evalRuns[0]).toMatchObject({ orgId, suiteId, state: "passed", failedCount: 0 });
|
||||
expect(state.prBotGates.find((gate) => gate.id === gateId)?.enabled).toBe(false);
|
||||
expect(state.auditEvents.some((event) => event.entityType === "pr_bot_gate" && event.action === "updated")).toBe(true);
|
||||
connection.close();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user