| 1 | package main |
| 2 | |
| 3 | import ( |
| 4 | "testing" |
| 5 | |
| 6 | "reasonix/desktop/internal/sessionui" |
| 7 | "reasonix/internal/provider" |
| 8 | ) |
| 9 | |
| 10 | func TestComposerReupgradeDetectsHistoryWithoutSubmissionReceipts(t *testing.T) { |
| 11 | a, ref := lifecycleFixture(t) |
| 12 | t.Cleanup(func() { _ = a.sessionUIStore().Close() }) |
| 13 | initial, err := a.GetSessionComposerState(ref) |
| 14 | if err != nil { |
| 15 | t.Fatal(err) |
| 16 | } |
| 17 | saved, err := a.SaveSessionComposerState(SessionComposerSaveRequest{Ref: ref, ExpectedRevision: initial.Revision, ContentVersion: 1, ContentJSON: `{"text":"unsent before downgrade"}`}) |
| 18 | if err != nil { |
| 19 | t.Fatal(err) |
| 20 | } |
| 21 | path := a.sessionUIStore().Path() |
| 22 | if err := a.sessionUIStore().Close(); err != nil { |
| 23 | t.Fatal(err) |
| 24 | } |
| 25 | // Simulate an old/imported history writer that does not know session-ui or |
| 26 | // attach a SubmissionID. The independent input file must remain unchanged. |
| 27 | binding, err := a.desktopSessionService("").Open(t.Context(), ref) |
| 28 | if err != nil { |
| 29 | t.Fatal(err) |
| 30 | } |
| 31 | appendSessionTestMessage(t, binding.Runtime(), "old-writer", provider.Message{ID: "old-writer-user", Role: provider.RoleUser, Content: "continued in old version"}) |
| 32 | if err := binding.Release(t.Context()); err != nil { |
| 33 | t.Fatal(err) |
| 34 | } |
| 35 | a.sessionUI = sessionui.New(path) |
| 36 | recovered, err := a.GetSessionComposerState(ref) |
| 37 | if err != nil || !recovered.HistoryChanged || recovered.ContentJSON != saved.ContentJSON { |
| 38 | t.Fatalf("unsafe reupgrade: %+v %v", recovered, err) |
| 39 | } |
| 40 | if _, err := a.BeginSessionComposerSubmission(ref, recovered.Revision, "must-not-send", `{}`); err == nil { |
| 41 | t.Fatal("unreviewed old input became sendable") |
| 42 | } |
| 43 | } |
| 44 | |
| 45 | func TestComposerArchiveRestoreAndPurgeKeepLifecycleOwnership(t *testing.T) { |
| 46 | a, ref := lifecycleFixture(t) |
| 47 | t.Cleanup(func() { _ = a.sessionUIStore().Close() }) |
| 48 | input, err := a.SaveSessionComposerState(SessionComposerSaveRequest{Ref: ref, ExpectedRevision: "0", ContentVersion: 1, ContentJSON: `{"text":"archive input","attachments":[{"path":"missing-file"}]}`}) |
| 49 | if err != nil { |
| 50 | t.Fatal(err) |
| 51 | } |
| 52 | if _, err := a.ArchiveSessionTarget(SessionSelector{Ref: &ref}); err != nil { |
| 53 | t.Fatal(err) |
| 54 | } |
| 55 | archived, err := a.GetSessionComposerState(ref) |
| 56 | if err != nil || archived.ContentJSON != input.ContentJSON { |
| 57 | t.Fatalf("archive lost input: %+v %v", archived, err) |
| 58 | } |
| 59 | if _, err := a.RestoreSessionTarget(SessionSelector{Ref: &ref}); err != nil { |
| 60 | t.Fatal(err) |
| 61 | } |
| 62 | restored, err := a.GetSessionComposerState(ref) |
| 63 | if err != nil || restored.ContentJSON != input.ContentJSON { |
| 64 | t.Fatalf("restore lost input: %+v %v", restored, err) |
| 65 | } |
| 66 | if _, err := a.ArchiveSessionTarget(SessionSelector{Ref: &ref}); err != nil { |
| 67 | t.Fatal(err) |
| 68 | } |
| 69 | if err := a.PurgeCanonicalSession(ref); err != nil { |
| 70 | t.Fatal(err) |
| 71 | } |
| 72 | row, err := a.sessionUIStore().Get(t.Context(), "composer", composerRecordKey(ref)) |
| 73 | if err != nil || row.Revision != "0" { |
| 74 | t.Fatalf("purge retained input: %+v %v", row, err) |
| 75 | } |
| 76 | if _, err := a.SaveSessionComposerState(SessionComposerSaveRequest{Ref: ref, ExpectedRevision: "0", ContentVersion: 1, ContentJSON: `{"text":"late save"}`}); err == nil { |
| 77 | t.Fatal("late save revived purged input") |
| 78 | } |
| 79 | } |
| 80 | |
| 81 | func TestComposerSubmissionIdentityCannotClearAnotherInputVersion(t *testing.T) { |
| 82 | a, ref := lifecycleFixture(t) |
| 83 | t.Cleanup(func() { _ = a.sessionUIStore().Close() }) |
| 84 | saved, err := a.SaveSessionComposerState(SessionComposerSaveRequest{Ref: ref, ExpectedRevision: "0", ContentVersion: 1, ContentJSON: `{"text":"first"}`}) |
| 85 | if err != nil { |
| 86 | t.Fatal(err) |
| 87 | } |
| 88 | pending, err := a.BeginSessionComposerSubmission(ref, saved.Revision, "fixed-submission", `{"submit":"first"}`) |
| 89 | if err != nil { |
| 90 | t.Fatal(err) |
| 91 | } |
| 92 | if repeated, err := a.BeginSessionComposerSubmission(ref, saved.Revision, "fixed-submission", `{"submit":"first"}`); err != nil || repeated.Revision != pending.Revision { |
| 93 | t.Fatalf("identical retry changed association: %+v %v", repeated, err) |
| 94 | } |
| 95 | if _, err := a.BeginSessionComposerSubmission(ref, saved.Revision, "fixed-submission", `{"submit":"different"}`); err == nil { |
| 96 | t.Fatal("changed request reused frozen identity") |
| 97 | } |
| 98 | done, err := a.CompleteSessionComposerSubmission(ref, "fixed-submission", "accepted") |
| 99 | if err != nil { |
| 100 | t.Fatal(err) |
| 101 | } |
| 102 | next, err := a.SaveSessionComposerState(SessionComposerSaveRequest{Ref: ref, ExpectedRevision: done.Revision, ContentVersion: 1, ContentJSON: `{"text":"second"}`}) |
| 103 | if err != nil { |
| 104 | t.Fatal(err) |
| 105 | } |
| 106 | if _, err := a.BeginSessionComposerSubmission(ref, next.Revision, "fixed-submission", `{"submit":"second"}`); err == nil { |
| 107 | t.Fatal("old receipt can clear a new input") |
| 108 | } |
| 109 | late, err := a.CompleteSessionComposerSubmission(ref, "fixed-submission", "accepted") |
| 110 | if err != nil || late.ContentJSON != next.ContentJSON { |
| 111 | t.Fatalf("late acceptance cleared new input: %+v %v", late, err) |
| 112 | } |
| 113 | } |
| 114 |