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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions archiveos-ai/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,28 @@ dependencies {
tasks.named("test") {
useJUnitPlatform()
}

sourceSets {
postgresIntegrationTest {
java.srcDir "src/postgresIntegrationTest/java"
resources.srcDir "src/postgresIntegrationTest/resources"
compileClasspath += sourceSets.main.output + configurations.testRuntimeClasspath
runtimeClasspath += output + compileClasspath
}
}

configurations {
postgresIntegrationTestImplementation.extendsFrom testImplementation
postgresIntegrationTestRuntimeOnly.extendsFrom testRuntimeOnly
}

tasks.register("postgresIntegrationTest", Test) {
description = "Runs PostgreSQL 16 integration tests. Requires -Darchiveos.pgtest.url."
group = "verification"
testClassesDirs = sourceSets.postgresIntegrationTest.output.classesDirs
classpath = sourceSets.postgresIntegrationTest.runtimeClasspath
useJUnitPlatform()
["archiveos.pgtest.url", "archiveos.pgtest.user", "archiveos.pgtest.password"].each { key ->
if (System.getProperty(key) != null) systemProperty key, System.getProperty(key)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -104,20 +104,26 @@ public Map<String, Object> inboxState(String id) {

public Map<String, Object> updateInboxState(String id, String status, Map<String, Object> metadata) {
ensureInboxStateTable();
String acknowledged = "acknowledged".equals(status) ? "now()" : "acknowledged_at";
String resolved = "resolved".equals(status) ? "now()" : "resolved_at";
return jdbc.queryForObject("""
insert into public.pm_inbox_item_states(id, status, acknowledged_at, resolved_at, metadata)
insert into public.pm_inbox_item_states as current_state (id, status, acknowledged_at, resolved_at, metadata)
values (?, ?, case when ? = 'acknowledged' then now() else null end,
case when ? = 'resolved' then now() else null end, ?::jsonb)
on conflict (id) do update set
status = excluded.status,
acknowledged_at = %s,
resolved_at = %s,
acknowledged_at = case
when excluded.status = 'acknowledged' then now()
else current_state.acknowledged_at
end,
resolved_at = case
when excluded.status = 'resolved' then now()
else current_state.resolved_at
end,
updated_at = now(),
metadata = pm_inbox_item_states.metadata || excluded.metadata
returning id, status, acknowledged_at, resolved_at, updated_at, metadata::text as metadata_json
""".formatted(acknowledged, resolved), this::inboxStateRow, id, status, status, status, Json.write(metadata == null ? Map.of() : metadata));
metadata = current_state.metadata || excluded.metadata
returning current_state.id, current_state.status, current_state.acknowledged_at,
current_state.resolved_at, current_state.updated_at,
current_state.metadata::text as metadata_json
""", this::inboxStateRow, id, status, status, status, Json.write(metadata == null ? Map.of() : metadata));
}

public void recordTimeline(String eventType, String status, String title, String summary, String systemId, String referenceId, Map<String, Object> metadata) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
package com.archiveos.ai.managed;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;

import com.archiveos.ai.approval.ExternalApprovalRepository;
import java.util.List;
import java.util.Map;
import javax.sql.DataSource;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.springframework.transaction.support.TransactionTemplate;

class ManagedSystemsPostgresIntegrationTest {
private static final String INBOX_ID = "daily-report-report-1";

private JdbcTemplate jdbc;
private ManagedSystemsRepository repository;
private TransactionTemplate transactions;

@BeforeEach
void setUp() {
String url = requiredProperty("archiveos.pgtest.url");
DataSource dataSource = new DriverManagerDataSource(url,
System.getProperty("archiveos.pgtest.user", "postgres"),
System.getProperty("archiveos.pgtest.password", ""));
jdbc = new JdbcTemplate(dataSource);
jdbc.execute("""
create table if not exists public.pm_inbox_item_states (
id text primary key,
status text not null check (status in ('open', 'acknowledged', 'resolved')),
acknowledged_at timestamptz,
resolved_at timestamptz,
updated_at timestamptz not null default now(),
metadata jsonb not null default '{}'::jsonb
)
""");
jdbc.execute("""
create table if not exists public.runtime_timeline (
id bigserial primary key,
event_type text not null,
status text not null,
title text not null,
summary text,
project_id text,
source text,
reference_id text,
metadata jsonb not null default '{}'::jsonb
)
""");
jdbc.update("delete from public.runtime_timeline");
jdbc.update("delete from public.pm_inbox_item_states");
repository = new ManagedSystemsRepository(jdbc);
transactions = new TransactionTemplate(new DataSourceTransactionManager(dataSource));
}

@AfterEach
void cleanUp() {
jdbc.update("delete from public.runtime_timeline");
jdbc.update("delete from public.pm_inbox_item_states");
}

@Test
void acknowledgeAndResolveUseQualifiedPostgresUpsertAndPreserveTimestamps() {
ManagedSystemsService service = inboxService();

Map<String, Object> acknowledged = inTransaction(() -> service.acknowledge(INBOX_ID));
String acknowledgedAt = (String) acknowledged.get("acknowledged_at");
assertThat(acknowledged).containsEntry("status", "acknowledged");
assertThat(acknowledgedAt).isNotNull();
assertThat(acknowledged.get("resolved_at")).isNull();

Map<String, Object> resolved = inTransaction(() -> service.resolve(INBOX_ID));
assertThat(resolved).containsEntry("status", "resolved");
assertThat(resolved.get("acknowledged_at")).isEqualTo(acknowledgedAt);
assertThat(resolved.get("resolved_at")).isInstanceOf(String.class);
assertThat(timelineCount(INBOX_ID)).isEqualTo(2);
}

@Test
void repeatedAcknowledgementAndResolutionAreIdempotentAndDoNotDuplicateTimeline() {
ManagedSystemsService service = inboxService();

Map<String, Object> acknowledged = inTransaction(() -> service.acknowledge(INBOX_ID));
Map<String, Object> acknowledgedAgain = inTransaction(() -> service.acknowledge(INBOX_ID));
assertThat(acknowledgedAgain).isEqualTo(acknowledged);
assertThat(timelineCount(INBOX_ID)).isEqualTo(1);

Map<String, Object> resolved = inTransaction(() -> service.resolve(INBOX_ID));
Map<String, Object> resolvedAgain = inTransaction(() -> service.resolve(INBOX_ID));
Map<String, Object> afterResolvedAcknowledgement = inTransaction(() -> service.acknowledge(INBOX_ID));
assertThat(resolvedAgain).isEqualTo(resolved);
assertThat(afterResolvedAcknowledgement).isEqualTo(resolved);
assertThat(timelineCount(INBOX_ID)).isEqualTo(2);
}

@Test
void newResolutionSucceedsAndExistingMetadataIsMerged() {
Map<String, Object> resolved = repository.updateInboxState("new-resolution", "resolved", Map.of("newKey", "newValue"));
assertThat(resolved).containsEntry("status", "resolved");
assertThat(resolved.get("acknowledged_at")).isNull();
assertThat(resolved.get("resolved_at")).isInstanceOf(String.class);

repository.updateInboxState("metadata-item", "acknowledged", Map.of("preserved", "value"));
Map<String, Object> merged = repository.updateInboxState("metadata-item", "resolved", Map.of("newKey", "newValue"));
@SuppressWarnings("unchecked")
Map<String, Object> metadata = (Map<String, Object>) merged.get("metadata");
assertThat(metadata).containsEntry("preserved", "value").containsEntry("newKey", "newValue");
}

@Test
void failedTransactionLeavesNeitherInboxStateNorTimeline() {
assertThatThrownBy(() -> transactions.executeWithoutResult(status -> {
repository.updateInboxState("rollback-item", "acknowledged", Map.of("action", "test"));
repository.recordTimeline("approval", "success", "rollback", "rollback", "archive-os", "rollback-item", Map.of());
throw new IllegalStateException("rollback");
})).isInstanceOf(IllegalStateException.class);

assertThat(repository.inboxState("rollback-item")).isNull();
assertThat(timelineCount("rollback-item")).isZero();
}

private ManagedSystemsService inboxService() {
ExternalApprovalRepository approvals = mock(ExternalApprovalRepository.class);
ManagedSystemsService service = Mockito.spy(new ManagedSystemsService(repository, approvals));
doReturn(List.of(Map.of("id", INBOX_ID))).when(service).pmInbox();
return service;
}

private Map<String, Object> inTransaction(java.util.concurrent.Callable<Map<String, Object>> callback) {
return transactions.execute(status -> {
try {
return callback.call();
} catch (Exception error) {
throw new IllegalStateException(error);
}
});
}

private long timelineCount(String referenceId) {
Long count = jdbc.queryForObject("select count(*) from public.runtime_timeline where reference_id = ?", Long.class, referenceId);
return count == null ? 0 : count;
}

private String requiredProperty(String name) {
String value = System.getProperty(name);
if (value == null || value.isBlank()) throw new IllegalStateException(name + " is required for PostgreSQL integration tests.");
return value;
}
}
Loading