feat: expand companion platform, device controls, and memoir workflows
ci / verify (push) Canceled after 0s
ci / verify (push) Canceled after 0s
Add customer and operator login, BLE provisioning, device fleet controls, call memory, memoir generation, and refreshed family and maintenance portals. Include API contracts, migrations, firmware sources, tests, assets, and deployment documentation.
This commit is contained in:
@@ -2,8 +2,10 @@ package privacy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"data-terminal-cloud/internal/cache"
|
||||
"data-terminal-cloud/internal/database"
|
||||
@@ -21,9 +23,13 @@ const (
|
||||
|
||||
type privacyDeleteStore struct {
|
||||
removed []string
|
||||
failure error
|
||||
}
|
||||
|
||||
func (s *privacyDeleteStore) Remove(_ context.Context, reference string) error {
|
||||
if s.failure != nil {
|
||||
return s.failure
|
||||
}
|
||||
s.removed = append(s.removed, reference)
|
||||
return nil
|
||||
}
|
||||
@@ -74,7 +80,7 @@ func TestDeleteProcessorReconcilesAllSubsystemsAndCreatesTombstone(t *testing.T)
|
||||
if _, err := pool.Exec(ctx, `INSERT INTO privacy_subtask (
|
||||
public_id,privacy_request_id,subsystem,target_type,target_ref
|
||||
) SELECT gen_random_uuid(),$1::uuid,subsystem,'subject_data',subsystem
|
||||
FROM unnest(ARRAY['redis','export_files','object_storage','pgvector','provider_index','postgresql']) AS subsystem`, privacyDeleteRequestID); err != nil {
|
||||
FROM unnest(ARRAY['redis','export_files','object_storage','pgvector','provider_index','postgresql','call_memory']) AS subsystem`, privacyDeleteRequestID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -85,6 +91,50 @@ func TestDeleteProcessorReconcilesAllSubsystemsAndCreatesTombstone(t *testing.T)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
store.failure = outbox.RetryAfter(errors.New("provider deletion rate limited"), time.Minute)
|
||||
if err := processor.Handle(ctx, outbox.Event{AggregateID: privacyDeleteRequestID}); err == nil || !outbox.IsRetryable(err) || outbox.RetryDelay(err) != time.Minute {
|
||||
t.Fatalf("dependency failure lost retry metadata: %v", err)
|
||||
}
|
||||
var failureStatus, failedSubsystem, taskStatus, lastError string
|
||||
var nextAttempt time.Time
|
||||
if err := pool.QueryRow(ctx, `SELECT pr.status,pr.result_summary->>'failed_subsystem',ps.status,ps.last_error,ps.next_attempt_at FROM privacy_request pr JOIN privacy_subtask ps ON ps.privacy_request_id=pr.id AND ps.subsystem='export_files' WHERE pr.id=$1::uuid`, privacyDeleteRequestID).Scan(&failureStatus, &failedSubsystem, &taskStatus, &lastError, &nextAttempt); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if failureStatus != "PARTIAL_FAILED" || failedSubsystem != "export_files" || taskStatus != "FAILED" || lastError != "provider deletion rate limited" || time.Until(nextAttempt) < 55*time.Second {
|
||||
t.Fatalf("failure was hidden or cooldown lost: request=%s subsystem=%s task=%s error=%q next=%v", failureStatus, failedSubsystem, taskStatus, lastError, nextAttempt)
|
||||
}
|
||||
// A dependency without Retry-After must use the same minimum cooldown in
|
||||
// the durable receipt and the error returned to the outbox worker.
|
||||
store.failure = errors.New("temporary object removal failure")
|
||||
if err := processor.Handle(ctx, outbox.Event{AggregateID: privacyDeleteRequestID}); err == nil || outbox.RetryDelay(err) != 30*time.Second {
|
||||
t.Fatalf("ordinary dependency failure did not preserve the receipt cooldown: %v", err)
|
||||
}
|
||||
var redisAttempts, exportAttempts int
|
||||
if err := pool.QueryRow(ctx, `SELECT attempts FROM privacy_subtask WHERE privacy_request_id=$1::uuid AND subsystem='redis'`, privacyDeleteRequestID).Scan(&redisAttempts); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := pool.QueryRow(ctx, `SELECT attempts,next_attempt_at FROM privacy_subtask WHERE privacy_request_id=$1::uuid AND subsystem='export_files'`, privacyDeleteRequestID).Scan(&exportAttempts, &nextAttempt); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if redisAttempts != 1 || exportAttempts != 2 || time.Until(nextAttempt) < 25*time.Second {
|
||||
t.Fatalf("retry repeated completed work or advertised a different cooldown: redis=%d export=%d next=%v", redisAttempts, exportAttempts, nextAttempt)
|
||||
}
|
||||
store.failure = nil
|
||||
if _, err := pool.Exec(ctx, `UPDATE memory_user_state SET legacy_projection_pending=true WHERE subject_id=$1::uuid`, privacyDeleteSubjectID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := processor.Handle(ctx, outbox.Event{AggregateID: privacyDeleteRequestID}); err == nil || outbox.RetryDelay(err) != time.Minute {
|
||||
t.Fatalf("unfinished call memory cleanup was acknowledged: %v", err)
|
||||
}
|
||||
if err := pool.QueryRow(ctx, `SELECT pr.status,pr.result_summary->>'failed_subsystem',ps.last_error FROM privacy_request pr JOIN privacy_subtask ps ON ps.privacy_request_id=pr.id AND ps.subsystem='call_memory' WHERE pr.id=$1::uuid`, privacyDeleteRequestID).Scan(&failureStatus, &failedSubsystem, &lastError); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if failureStatus != "PARTIAL_FAILED" || failedSubsystem != "call_memory" || lastError != "call memory deletion pending: legacy_projection" {
|
||||
t.Fatalf("cleanup recovery reason was hidden: status=%s subsystem=%s reason=%q", failureStatus, failedSubsystem, lastError)
|
||||
}
|
||||
if _, err := pool.Exec(ctx, `UPDATE memory_user_state SET legacy_projection_pending=false WHERE subject_id=$1::uuid`, privacyDeleteSubjectID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := processor.Handle(ctx, outbox.Event{AggregateID: privacyDeleteRequestID}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -110,12 +160,18 @@ func TestDeleteProcessorReconcilesAllSubsystemsAndCreatesTombstone(t *testing.T)
|
||||
if err := pool.QueryRow(ctx, `SELECT status,object_ref FROM export_task WHERE id='90000000-0000-0000-0000-000000000745'::uuid`).Scan(&exportStatus, &exportReference); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if requestStatus != "COMPLETED" || subjectStatus != "deleted" || preferredName != "[已删除]" || completed != 5 || skipped != 1 || tombstones != 1 || audits != 1 || len(store.removed) != 1 || store.removed[0] != "privacy-delete/export.zip" || exportStatus != "EXPIRED" || exportReference != nil {
|
||||
if requestStatus != "COMPLETED" || subjectStatus != "deleted" || preferredName != "[已删除]" || completed != 6 || skipped != 1 || tombstones != 1 || audits != 1 || len(store.removed) != 1 || store.removed[0] != "privacy-delete/export.zip" || exportStatus != "EXPIRED" || exportReference != nil {
|
||||
t.Fatalf("request=%s subject=%s name=%q completed=%d skipped=%d tombstones=%d audits=%d removed=%v", requestStatus, subjectStatus, preferredName, completed, skipped, tombstones, audits, store.removed)
|
||||
}
|
||||
if err := processor.Handle(ctx, outbox.Event{AggregateID: privacyDeleteRequestID}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := pool.QueryRow(ctx, `SELECT attempts FROM privacy_subtask WHERE privacy_request_id=$1::uuid AND subsystem='export_files'`, privacyDeleteRequestID).Scan(&exportAttempts); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if exportAttempts != 3 || len(store.removed) != 1 {
|
||||
t.Fatalf("completed deletion was executed again: attempts=%d removed=%d", exportAttempts, len(store.removed))
|
||||
}
|
||||
if err := processor.Handle(ctx, outbox.Event{AggregateID: "90000000-0000-0000-0000-000000000799"}); err != nil {
|
||||
t.Fatalf("missing deletion request error=%v", err)
|
||||
}
|
||||
@@ -128,6 +184,7 @@ func cleanupPrivacyDeleteFixture(t *testing.T, ctx context.Context, pool *pgxpoo
|
||||
`DELETE FROM audit_event WHERE action='privacy.delete.complete' AND resource_id='90000000-0000-0000-0000-000000000744'::uuid`,
|
||||
`DELETE FROM privacy_request WHERE id='90000000-0000-0000-0000-000000000743'::uuid`,
|
||||
`DELETE FROM export_task WHERE id='90000000-0000-0000-0000-000000000745'::uuid`,
|
||||
`DELETE FROM memory_user_state WHERE subject_id='90000000-0000-0000-0000-000000000741'::uuid`,
|
||||
`DELETE FROM subject_profile WHERE id='90000000-0000-0000-0000-000000000741'::uuid`,
|
||||
}
|
||||
for _, statement := range statements {
|
||||
|
||||
Reference in New Issue
Block a user