Q195: a change reloads only what is not already shown, and the snapshot stops carrying records the window never reads
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2f79e4261f
commit
56eac39b1b
31 files changed
+146
-214
No files matched your search
@@ -25,6 +25,10 @@ test("a record that breaks one page leaves the rest of the app working", async (
|
||||
db.prepare("insert into approvals(id, org_id, kind, title, details, risk, requested_by_type, state, created_at) values ('app_bad', ?, 'task_risk', 'Broken request', 'd', 'unknown', 'system', 'pending', ?)")
|
||||
.run(orgId, new Date().toISOString());
|
||||
db.close();
|
||||
// Written behind the app's back, so the test announces it as the app's own changes are.
|
||||
await app.evaluate(({ BrowserWindow }) => {
|
||||
BrowserWindow.getAllWindows()[0]!.webContents.send("agentcompany:renderer-event", { type: "snapshot-changed", version: Number.MAX_SAFE_INTEGER });
|
||||
});
|
||||
|
||||
await page.getByRole("button", { name: "Approvals" }).click();
|
||||
const failure = page.getByRole("alert").filter({ hasText: "This page could not be shown." });
|
||||
|
||||
@@ -54,7 +54,7 @@ test("data that can no longer be updated says so, from when it is, and recovers"
|
||||
db.close();
|
||||
};
|
||||
const announceChange = () => app.evaluate(({ BrowserWindow }) => {
|
||||
BrowserWindow.getAllWindows()[0]!.webContents.send("agentcompany:renderer-event", { type: "snapshot-changed" });
|
||||
BrowserWindow.getAllWindows()[0]!.webContents.send("agentcompany:renderer-event", { type: "snapshot-changed", version: Number.MAX_SAFE_INTEGER });
|
||||
});
|
||||
try {
|
||||
const page = await app.firstWindow();
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import type { DatabaseSync } from "node:sqlite";
|
||||
import { camel } from "../../src/main/services/snapshot-service";
|
||||
|
||||
/** A table's rows with camel-cased keys, for asserting backend records the app no longer ships to the window. */
|
||||
export function tableRows(db: DatabaseSync, table: string, orderBy: string, booleans: string[] = []): Array<Record<string, unknown>> {
|
||||
return (db.prepare(`select * from ${table} order by ${orderBy}`).all() as Array<Record<string, unknown>>).map((row) => {
|
||||
const next = camel(row as Record<string, string | number | null>);
|
||||
for (const key of booleans) next[key] = next[key] === 1;
|
||||
return next;
|
||||
});
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { tableRows } from "../helpers/table-rows";
|
||||
import { mkdirSync, writeFileSync } from "node:fs";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { tmpdir } from "node:os";
|
||||
@@ -104,9 +105,9 @@ describe("collaboration integration flow", () => {
|
||||
.get() as { compiled_prompt: string } | undefined;
|
||||
if (!snapshotRecord) throw new Error("Skill runtime snapshot was not created.");
|
||||
expect(state.approvals.find((approval) => approval.id === approvalId)?.state).toBe(APPROVAL_STATE.APPROVED);
|
||||
expect(state.approvalAssignments.some((assignment) => assignment.approvalId === approvalId)).toBe(true);
|
||||
expect(tableRows(connection.db, "approval_assignments", "created_at desc").some((assignment) => assignment.approvalId === approvalId)).toBe(true);
|
||||
expect(snapshotRecord.compiled_prompt).toContain("Release Gate");
|
||||
expect(state.operationalEvents.some((event) => event.signalKey === "skill_resolve_success")).toBe(true);
|
||||
expect(tableRows(connection.db, "operational_events", "created_at desc", ["success"]).some((event) => event.signalKey === "skill_resolve_success")).toBe(true);
|
||||
connection.close();
|
||||
});
|
||||
});
|
||||
@@ -1,3 +1,4 @@
|
||||
import { tableRows } from "../helpers/table-rows";
|
||||
import { writeFileSync } from "node:fs";
|
||||
import { createHash, randomUUID } from "node:crypto";
|
||||
import { tmpdir } from "node:os";
|
||||
@@ -567,10 +568,10 @@ describe("chat governance", () => {
|
||||
const state = snapshot.load();
|
||||
const thread = state.chatThreads.find((item) => item.id === threadId);
|
||||
expect(thread).toMatchObject({ boardState: "takeover", controlledByType: "agent", controlledById: agentId });
|
||||
expect(state.boardInterventions.map((item) => item.action)).toEqual(["freeze", "rollback", "takeover", "force_approval"]);
|
||||
expect(tableRows(connection.db, "board_interventions", "created_at asc").map((item) => item.action)).toEqual(["freeze", "rollback", "takeover", "force_approval"]);
|
||||
const forceApproval = state.approvals.find((item) => item.kind === "board_force_approval");
|
||||
if (!forceApproval) throw new Error("Board force approval was not created.");
|
||||
const forceIntervention = state.boardInterventions.find((item) => item.action === "force_approval");
|
||||
const forceIntervention = tableRows(connection.db, "board_interventions", "created_at asc").find((item) => item.action === "force_approval");
|
||||
if (!forceIntervention) throw new Error("Board force approval intervention was not created.");
|
||||
expect(forceIntervention.approvalId).toBe(forceApproval.id);
|
||||
expect(state.chatSlashInvocations.some((item) => item.resultRef === forceIntervention.id)).toBe(true);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { tableRows } from "../helpers/table-rows";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
@@ -167,7 +168,7 @@ describe("compliance service", () => {
|
||||
expect(redactedApproval?.decidedByEmployeeId).toBeNull();
|
||||
expect(redactedSocialAccount?.authorizedEmployeeId).toBeNull();
|
||||
expect(state.socialAccountGrants).toHaveLength(0);
|
||||
expect(state.providerAuthSessions).toHaveLength(0);
|
||||
expect(tableRows(connection.db, "provider_auth_sessions", "created_at desc")).toHaveLength(0);
|
||||
expect(state.complianceExports[0]?.exportType).toBe("gdpr_delete");
|
||||
expect(state.complianceExports[0]?.requestedByEmployeeId).toBeNull();
|
||||
expect(state.auditEvents.some((event) => event.entityType === "compliance_export" && event.action === "gdpr_deleted")).toBe(true);
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
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 { SnapshotService } from "../../src/main/services/snapshot-service";
|
||||
import { bumpDataVersion, currentDataVersion } from "../../src/main/services/data-version";
|
||||
import { supersedes } from "../../src/renderer/src/app/snapshot-order";
|
||||
|
||||
describe("data version", () => {
|
||||
test("a snapshot read after a change carries that change's version, so its own change event needs no reload", () => {
|
||||
const connection = new DatabaseConnection(join(tmpdir(), `ac-data-version-${randomUUID()}.db`));
|
||||
const snapshot = new SnapshotService(connection.db);
|
||||
const before = snapshot.load().dataVersion;
|
||||
const announced = bumpDataVersion();
|
||||
const after = snapshot.load();
|
||||
expect(after.dataVersion).toBe(announced);
|
||||
expect(announced).toBeGreaterThan(before);
|
||||
// A later change is newer than what is shown, so it is reloaded.
|
||||
expect(bumpDataVersion()).toBeGreaterThan(after.dataVersion);
|
||||
expect(currentDataVersion()).toBe(after.dataVersion + 1);
|
||||
// Records the window never reads are not shipped with every reload.
|
||||
expect(Object.keys(after)).not.toEqual(expect.arrayContaining(["operationalEvents", "featureFlags", "approvalAssignments", "roleGrants"]));
|
||||
connection.close();
|
||||
});
|
||||
});
|
||||
|
||||
describe("snapshot order", () => {
|
||||
test("a late answer read at an older version never replaces newer data on screen", () => {
|
||||
expect(supersedes(2, 1)).toBe(false);
|
||||
expect(supersedes(2, 2)).toBe(true);
|
||||
expect(supersedes(2, 3)).toBe(true);
|
||||
expect(supersedes(-1, 0)).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,3 +1,4 @@
|
||||
import { tableRows } from "../helpers/table-rows";
|
||||
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
@@ -55,13 +56,13 @@ describe("domain services", () => {
|
||||
expect(state.organizations).toHaveLength(1);
|
||||
expect(state.employees[0]?.displayName).toBe("Dana Lee");
|
||||
expect(state.providers.length).toBeGreaterThanOrEqual(6);
|
||||
expect(state.employeeIdentities).toHaveLength(1);
|
||||
expect(state.roleGrants.map((grant) => grant.roleKey).sort()).toEqual(["approval_decider", "hiring_manager", "org_admin"]);
|
||||
expect(state.orgDepartments.map((department) => department.name)).toContain("Engineering");
|
||||
expect(tableRows(connection.db, "employee_identities", "created_at desc", ["active"])).toHaveLength(1);
|
||||
expect(tableRows(connection.db, "role_grants", "created_at desc").map((grant) => grant.roleKey).sort()).toEqual(["approval_decider", "hiring_manager", "org_admin"]);
|
||||
expect(tableRows(connection.db, "org_departments", "name asc").map((department) => department.name)).toContain("Engineering");
|
||||
expect(state.hiringPolicies.some((policy) => policy.departmentName === "Engineering")).toBe(true);
|
||||
expect(state.approvalRoutingRules[0]).toMatchObject({ kind: "*", minimumRisk: "low", routeToType: "employee" });
|
||||
expect(state.featureFlags.map((flag) => flag.key)).toContain("board_intervention");
|
||||
expect(state.sloTargets.map((target) => target.signalKey)).toContain("provider_request_success");
|
||||
expect(tableRows(connection.db, "approval_routing_rules", "created_at asc")[0]).toMatchObject({ kind: "*", minimumRisk: "low", routeToType: "employee" });
|
||||
expect(tableRows(connection.db, "feature_flags", "key asc", ["enabled"]).map((flag) => flag.key)).toContain("board_intervention");
|
||||
expect(tableRows(connection.db, "slo_targets", "signal_key asc").map((target) => target.signalKey)).toContain("provider_request_success");
|
||||
expect(state.chatSpaces.map((space) => space.name)).toContain("board");
|
||||
expect(state.policyRules[0]?.action).toBe("task_create");
|
||||
expect(state.auditEvents[0]?.action).toBe("created");
|
||||
@@ -100,7 +101,7 @@ describe("domain services", () => {
|
||||
const state = snapshot.load();
|
||||
expect(state.tasks).toHaveLength(1);
|
||||
expect(state.approvals).toHaveLength(1);
|
||||
expect(state.approvalAssignments).toHaveLength(1);
|
||||
expect(tableRows(connection.db, "approval_assignments", "created_at desc")).toHaveLength(1);
|
||||
expect(state.approvals[0]?.state).toBe("pending");
|
||||
expect(state.approvals[0]).toMatchObject({ sourceEntityType: "task", sourceEntityId: state.tasks[0]?.id });
|
||||
connection.close();
|
||||
@@ -140,7 +141,7 @@ describe("domain services", () => {
|
||||
expect(state.approvals).toHaveLength(1);
|
||||
expect(state.approvals[0]?.state).toBe("approved");
|
||||
expect(state.approvals[0]?.decidedByEmployeeId).toBe(employeeId);
|
||||
expect(state.approvalAssignments).toHaveLength(0);
|
||||
expect(tableRows(connection.db, "approval_assignments", "created_at desc")).toHaveLength(0);
|
||||
connection.close();
|
||||
});
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { tableRows } from "../helpers/table-rows";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
@@ -58,8 +59,8 @@ describe("identity access service", () => {
|
||||
});
|
||||
|
||||
const state = snapshot.load();
|
||||
expect(state.employeeIdentities.some((identity) => identity.source === "sso" && identity.email === "dana@example.com")).toBe(true);
|
||||
expect(state.roleGrants.some((grant) => grant.principalId === employeeId && grant.roleKey === ROLE_KEY.APPROVAL_DECIDER)).toBe(true);
|
||||
expect(tableRows(connection.db, "employee_identities", "created_at desc", ["active"]).some((identity) => identity.source === "sso" && identity.email === "dana@example.com")).toBe(true);
|
||||
expect(tableRows(connection.db, "role_grants", "created_at desc").some((grant) => grant.principalId === employeeId && grant.roleKey === ROLE_KEY.APPROVAL_DECIDER)).toBe(true);
|
||||
expect(state.auditEvents.some((event) => event.entityType === "employee_identity" && event.action === "synced")).toBe(true);
|
||||
connection.close();
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { tableRows } from "../helpers/table-rows";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
@@ -7,7 +8,6 @@ import { OperationsService } from "../../src/main/services/operations-service";
|
||||
import { OrganizationService } from "../../src/main/services/organization-service";
|
||||
import { ProviderService } from "../../src/main/services/provider-service";
|
||||
import { providerAdapters, type ProviderAdapterRegistry } from "../../src/main/services/provider-adapters";
|
||||
import { SnapshotService } from "../../src/main/services/snapshot-service";
|
||||
import { PROVIDER_DATA_POLICY, PROVIDER_EXECUTION_MODE } from "../../src/shared/domain";
|
||||
|
||||
function createConnection(): DatabaseConnection {
|
||||
@@ -37,20 +37,18 @@ describe("operations service", () => {
|
||||
test("seeds feature flags and SLO targets, and no drill that scores configuration as fault handling", () => {
|
||||
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.featureFlags.map((flag) => flag.key).sort()).toEqual([
|
||||
expect(tableRows(connection.db, "feature_flags", "key asc", ["enabled"]).map((flag) => flag.key).sort()).toEqual([
|
||||
"board_intervention",
|
||||
"provider_background_execution",
|
||||
"social_publish_approval",
|
||||
]);
|
||||
// Every SLO target is fed by a runtime source; none waits on a signal only a drill would write.
|
||||
expect(state.sloTargets.map((target) => target.signalKey).sort()).toEqual([
|
||||
expect(tableRows(connection.db, "slo_targets", "signal_key asc").map((target) => target.signalKey).sort()).toEqual([
|
||||
"approval_route_coverage", "migration_validation_success", "provider_request_success", "skill_resolve_success",
|
||||
]);
|
||||
expect(connection.db.prepare("select name from sqlite_master where type = 'table' and name like 'chaos_drill%'").all()).toEqual([]);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { tableRows } from "../helpers/table-rows";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
@@ -72,8 +73,8 @@ describe("org governance service", () => {
|
||||
});
|
||||
|
||||
const state = snapshot.load();
|
||||
expect(state.approvalAssignments).toHaveLength(1);
|
||||
expect(state.approvalAssignments[0]).toMatchObject({ approvalId, assigneeType: "employee" });
|
||||
expect(tableRows(connection.db, "approval_assignments", "created_at desc")).toHaveLength(1);
|
||||
expect(tableRows(connection.db, "approval_assignments", "created_at desc")[0]).toMatchObject({ approvalId, assigneeType: "employee" });
|
||||
expect(state.auditEvents.some((event) => event.entityType === "approval" && event.action === "routed")).toBe(true);
|
||||
connection.close();
|
||||
});
|
||||
@@ -107,8 +108,8 @@ describe("org governance service", () => {
|
||||
const state = snapshot.load();
|
||||
const approval = state.approvals.find((item) => item.kind === "agent_hire");
|
||||
expect(approval).toMatchObject({ orgId, title: "Review hire: Ada", risk: RISK_LEVEL.MEDIUM });
|
||||
expect(state.approvalAssignments).toHaveLength(1);
|
||||
expect(state.approvalAssignments[0]).toMatchObject({ approvalId: approval?.id, assigneeType: "employee" });
|
||||
expect(tableRows(connection.db, "approval_assignments", "created_at desc")).toHaveLength(1);
|
||||
expect(tableRows(connection.db, "approval_assignments", "created_at desc")[0]).toMatchObject({ approvalId: approval?.id, assigneeType: "employee" });
|
||||
connection.close();
|
||||
});
|
||||
});
|
||||
@@ -1,3 +1,4 @@
|
||||
import { tableRows } from "../helpers/table-rows";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
@@ -5,7 +6,6 @@ import { describe, expect, test } from "vitest";
|
||||
import { DatabaseConnection } from "../../src/main/database/connection";
|
||||
import { OrganizationService } from "../../src/main/services/organization-service";
|
||||
import { parseProviderRateLimitHeaders, ProviderRateLimitRegistry } from "../../src/main/services/provider-rate-limit-registry";
|
||||
import { SnapshotService } from "../../src/main/services/snapshot-service";
|
||||
|
||||
function createConnection(): DatabaseConnection {
|
||||
return new DatabaseConnection(join(tmpdir(), `agentcompany-provider-rate-limits-${randomUUID()}.db`));
|
||||
@@ -52,7 +52,6 @@ describe("provider rate limit registry", () => {
|
||||
test("persists observed quota state into snapshots", () => {
|
||||
const connection = createConnection();
|
||||
const organization = new OrganizationService(connection.db);
|
||||
const snapshot = new SnapshotService(connection.db);
|
||||
const registry = new ProviderRateLimitRegistry(connection.db);
|
||||
let orgId = "";
|
||||
let openAiProviderId = "";
|
||||
@@ -75,7 +74,7 @@ describe("provider rate limit registry", () => {
|
||||
});
|
||||
});
|
||||
|
||||
expect(snapshot.load().providerRateLimitObservations[0]).toMatchObject({
|
||||
expect(tableRows(connection.db, "provider_rate_limit_observations", "observed_at desc")[0]).toMatchObject({
|
||||
orgId,
|
||||
providerId: openAiProviderId,
|
||||
providerKey: "openai",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { tableRows } from "../helpers/table-rows";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { randomUUID } from "node:crypto";
|
||||
@@ -238,7 +239,7 @@ describe("provider service", () => {
|
||||
responseText: "api_key:gpt-5.4:Draft board memo",
|
||||
state: "succeeded",
|
||||
});
|
||||
expect(state.operationalEvents[0]).toMatchObject({ signalKey: "provider_request_success", source: "provider_gateway", success: true });
|
||||
expect(tableRows(connection.db, "operational_events", "created_at desc", ["success"])[0]).toMatchObject({ signalKey: "provider_request_success", source: "provider_gateway", success: true });
|
||||
expect(state.providers.find((provider) => provider.id === openAiProviderId)?.healthState).toBe("healthy");
|
||||
connection.close();
|
||||
});
|
||||
@@ -246,7 +247,6 @@ describe("provider service", () => {
|
||||
test("records provider response rate limit observations", async () => {
|
||||
const connection = createConnection();
|
||||
const organization = new OrganizationService(connection.db);
|
||||
const snapshot = new SnapshotService(connection.db);
|
||||
const adapters: ProviderAdapterRegistry = {
|
||||
...providerAdapters,
|
||||
openai: {
|
||||
@@ -281,7 +281,7 @@ describe("provider service", () => {
|
||||
instructions: null,
|
||||
});
|
||||
|
||||
expect(snapshot.load().providerRateLimitObservations[0]).toMatchObject({
|
||||
expect(tableRows(connection.db, "provider_rate_limit_observations", "observed_at desc")[0]).toMatchObject({
|
||||
orgId,
|
||||
providerId: openAiProviderId,
|
||||
providerKey: "openai",
|
||||
@@ -337,7 +337,7 @@ describe("provider service", () => {
|
||||
state: "failed",
|
||||
errorMessage: "rate limited",
|
||||
});
|
||||
expect(state.providerRateLimitObservations[0]).toMatchObject({
|
||||
expect(tableRows(connection.db, "provider_rate_limit_observations", "observed_at desc")[0]).toMatchObject({
|
||||
orgId,
|
||||
providerId: openAiProviderId,
|
||||
providerKey: "openai",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { tableRows } from "../helpers/table-rows";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { randomUUID } from "node:crypto";
|
||||
@@ -274,7 +275,6 @@ describe("social service", () => {
|
||||
const connection = createConnection();
|
||||
const organization = new OrganizationService(connection.db);
|
||||
const social = new SocialService(connection.db);
|
||||
const snapshot = new SnapshotService(connection.db);
|
||||
let orgId = "";
|
||||
let authorizerId = "";
|
||||
let approvalId = "";
|
||||
@@ -303,8 +303,7 @@ describe("social service", () => {
|
||||
.get(postId) as { approval_id: string }).approval_id;
|
||||
});
|
||||
|
||||
const state = snapshot.load();
|
||||
expect(state.approvalAssignments).toContainEqual(expect.objectContaining({
|
||||
expect(tableRows(connection.db, "approval_assignments", "created_at desc")).toContainEqual(expect.objectContaining({
|
||||
approvalId,
|
||||
assigneeType: ROUTE_TO_TYPE.EMPLOYEE,
|
||||
assigneeId: authorizerId,
|
||||
|
||||
Reference in new issue
Block a user