@@ -861,6 +861,84 @@ func TestCmdCloudUpgradeDoctorRequiresProjectAndIsDeterministic(t *testing.T) {
861861 })
862862}
863863
864+ func TestCmdSyncCloudPreflightsLegacyMutationPayloads (t * testing.T ) {
865+ stubExitWithPanic (t )
866+ stubRuntimeHooks (t )
867+
868+ cfg := testConfig (t )
869+ if err := saveCloudConfig (cfg , & cloudConfig {ServerURL : "https://cloud.example.test" }); err != nil {
870+ t .Fatalf ("save cloud config: %v" , err )
871+ }
872+
873+ s , err := store .New (cfg )
874+ if err != nil {
875+ t .Fatalf ("open store: %v" , err )
876+ }
877+ if err := s .CreateSession ("sync-legacy-s1" , "sync-legacy" , "/tmp/sync-legacy" ); err != nil {
878+ _ = s .Close ()
879+ t .Fatalf ("create session: %v" , err )
880+ }
881+ if _ , err := s .AddObservation (store.AddObservationParams {SessionID : "sync-legacy-s1" , Type : "decision" , Title : "Canonical title" , Content : "Canonical content" , Project : "sync-legacy" , Scope : "project" }); err != nil {
882+ _ = s .Close ()
883+ t .Fatalf ("add observation: %v" , err )
884+ }
885+ if err := s .EnrollProject ("sync-legacy" ); err != nil {
886+ _ = s .Close ()
887+ t .Fatalf ("enroll project: %v" , err )
888+ }
889+ _ = s .Close ()
890+
891+ db , err := sql .Open ("sqlite" , filepath .Join (cfg .DataDir , "engram.db" ))
892+ if err != nil {
893+ t .Fatalf ("open raw db: %v" , err )
894+ }
895+ defer db .Close ()
896+ var syncID string
897+ if err := db .QueryRow (`SELECT sync_id FROM observations WHERE session_id = ? ORDER BY id DESC LIMIT 1` , "sync-legacy-s1" ).Scan (& syncID ); err != nil {
898+ t .Fatalf ("lookup sync id: %v" , err )
899+ }
900+ legacyPayload := `{"sync_id":"` + syncID + `","session_id":"sync-legacy-s1","type":"decision","content":"legacy payload missing title","scope":"project"}`
901+ if _ , err := db .Exec (
902+ `INSERT INTO sync_mutations (target_key, entity, entity_key, op, payload, source, project) VALUES (?, ?, ?, ?, ?, ?, ?)` ,
903+ store .DefaultSyncTargetKey ,
904+ store .SyncEntityObservation ,
905+ syncID ,
906+ store .SyncOpUpsert ,
907+ legacyPayload ,
908+ store .SyncSourceLocal ,
909+ "sync-legacy" ,
910+ ); err != nil {
911+ t .Fatalf ("insert legacy mutation: %v" , err )
912+ }
913+
914+ exportCalled := false
915+ oldSyncExport := syncExport
916+ syncExport = func (_ * engramsync.Syncer , _ , _ string ) (* engramsync.SyncResult , error ) {
917+ exportCalled = true
918+ return & engramsync.SyncResult {}, nil
919+ }
920+ t .Cleanup (func () { syncExport = oldSyncExport })
921+
922+ withArgs (t , "engram" , "sync" , "--cloud" , "--project" , "sync-legacy" )
923+ _ , stderr , recovered := captureOutputAndRecover (t , func () { cmdSync (cfg ) })
924+ if _ , ok := recovered .(exitCode ); ! ok {
925+ t .Fatalf ("expected cloud sync preflight to fail loudly, got %v" , recovered )
926+ }
927+ if exportCalled {
928+ t .Fatal ("cloud sync must block before export/canonicalization" )
929+ }
930+ if ! strings .Contains (stderr , "legacy mutation payloads require repair before cloud sync" ) ||
931+ ! strings .Contains (stderr , "engram cloud upgrade doctor --project sync-legacy" ) ||
932+ ! strings .Contains (stderr , "engram cloud upgrade repair --project sync-legacy --apply" ) {
933+ t .Fatalf ("expected actionable legacy mutation guidance, got %q" , stderr )
934+ }
935+
936+ var persistedPayload string
937+ if err := db .QueryRow (`SELECT payload FROM sync_mutations WHERE project = ? AND payload = ?` , "sync-legacy" , legacyPayload ).Scan (& persistedPayload ); err != nil {
938+ t .Fatalf ("expected sync preflight not to auto-repair payload: %v" , err )
939+ }
940+ }
941+
864942func TestCmdCloudUpgradeBootstrapStatusAndRollbackSemantics (t * testing.T ) {
865943 stubExitWithPanic (t )
866944 stubRuntimeHooks (t )
0 commit comments