@1agh/maude 0.60.2 → 0.60.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/apps/studio/api.ts +14 -0
  2. package/apps/studio/client/app.jsx +44 -9
  3. package/apps/studio/client/panels/SyncPanel.jsx +53 -11
  4. package/apps/studio/context.ts +9 -0
  5. package/apps/studio/dist/client.bundle.js +525 -525
  6. package/apps/studio/http.ts +48 -7
  7. package/apps/studio/server.ts +11 -0
  8. package/apps/studio/sync/asset-pull.ts +210 -0
  9. package/apps/studio/sync/asset-push.ts +118 -75
  10. package/apps/studio/sync/connection-state.ts +23 -0
  11. package/apps/studio/sync/discovery.ts +139 -0
  12. package/apps/studio/sync/file-membership.ts +290 -0
  13. package/apps/studio/sync/file-pull.ts +330 -0
  14. package/apps/studio/sync/index.ts +995 -166
  15. package/apps/studio/sync/remote-docs.ts +98 -4
  16. package/apps/studio/sync/status.ts +25 -0
  17. package/apps/studio/sync/tombstone-apply.ts +131 -0
  18. package/apps/studio/test/cloud-managed-save-surfaces.test.ts +90 -0
  19. package/apps/studio/test/exporters/jobs.test.ts +10 -4
  20. package/apps/studio/test/git-cloud-posture.test.ts +11 -2
  21. package/apps/studio/test/sync-asset-pull.test.ts +161 -0
  22. package/apps/studio/test/sync-asset-push.test.ts +144 -3
  23. package/apps/studio/test/sync-attach-incremental.test.ts +527 -0
  24. package/apps/studio/test/sync-file-membership.test.ts +331 -0
  25. package/apps/studio/test/sync-file-pull.test.ts +333 -0
  26. package/apps/studio/test/sync-fresh-link-parity.test.ts +267 -0
  27. package/apps/studio/test/sync-panel-surface.test.ts +10 -0
  28. package/apps/studio/test/sync-remote-docs.test.ts +118 -8
  29. package/apps/studio/test/sync-resync-routes.test.ts +43 -0
  30. package/apps/studio/test/sync-status.test.ts +18 -0
  31. package/apps/studio/test/sync-supervisor.test.ts +4 -0
  32. package/apps/studio/test/sync-tombstone-apply.test.ts +111 -0
  33. package/apps/studio/test/sync-two-peer-discovery.test.ts +343 -0
  34. package/apps/studio/whats-new.json +18 -0
  35. package/cli/commands/doctor.mjs +141 -6
  36. package/cli/lib/gitignore-drift.mjs +149 -0
  37. package/cli/lib/gitignore-drift.test.mjs +156 -0
  38. package/package.json +8 -8
@@ -6,17 +6,22 @@
6
6
  // credential is read at call time; a failure is reported, never thrown.
7
7
 
8
8
  import { describe, expect, test } from 'bun:test';
9
- import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs';
9
+ import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
10
10
  import { tmpdir } from 'node:os';
11
11
  import { join } from 'node:path';
12
12
 
13
- import { listPushableAssets, pushAssets, putTimeoutMs } from '../sync/asset-push.ts';
13
+ import {
14
+ isPushableAssetRel,
15
+ listPushableAssets,
16
+ pushAssets,
17
+ putTimeoutMs,
18
+ } from '../sync/asset-push.ts';
14
19
 
15
20
  function scratchDesignRoot(): string {
16
21
  return mkdtempSync(join(tmpdir(), 'asset-push-'));
17
22
  }
18
23
 
19
- describe('listPushableAssets — every assets/ dir, asset extensions only', () => {
24
+ describe('listPushableAssets — classifier membership over the whole design root', () => {
20
25
  test('finds top-level content-addressed AND nested DS/brand assets', () => {
21
26
  const designRoot = scratchDesignRoot();
22
27
  // Class 1 — top-level content-addressed uploads (+ a .photo.json sidecar).
@@ -62,6 +67,51 @@ describe('listPushableAssets — every assets/ dir, asset extensions only', () =
62
67
  writeFileSync(join(designRoot, 'assets/a/b/c/d/e/f/g/x.png'), 'x');
63
68
  expect(listPushableAssets(designRoot)).toEqual([]);
64
69
  });
70
+
71
+ test('the file-plane widening: the RCA miss-list goes up, canvas lanes stay home', () => {
72
+ // feature-sync-file-plane — the sweep now carries what the fresh-link RCA
73
+ // named laneless: token stylesheets, docs, shared modules, underscore
74
+ // FILES. A canvas body and its named sidecars stay the CRDT lanes'.
75
+ const designRoot = scratchDesignRoot();
76
+ mkdirSync(join(designRoot, 'system/ds/preview'), { recursive: true });
77
+ writeFileSync(join(designRoot, 'config.json'), '{}');
78
+ writeFileSync(join(designRoot, 'system/ds/brand.css'), 'x');
79
+ writeFileSync(join(designRoot, 'system/ds/README.md'), 'x');
80
+ writeFileSync(join(designRoot, 'system/ds/preview/_brand-css.ts'), 'x');
81
+ writeFileSync(join(designRoot, 'system/ds/preview/_layout.css'), 'x');
82
+ writeFileSync(join(designRoot, 'system/ds/preview/specimen.tsx'), 'x');
83
+ writeFileSync(join(designRoot, 'system/ds/preview/specimen.css'), 'x'); // sibling css lane
84
+ writeFileSync(join(designRoot, 'system/ds/preview/specimen.meta.json'), '{}');
85
+ expect(listPushableAssets(designRoot, { syncFiles: true })).toEqual([
86
+ 'system/ds/README.md',
87
+ 'system/ds/brand.css',
88
+ 'system/ds/preview/_brand-css.ts',
89
+ 'system/ds/preview/_layout.css',
90
+ ]);
91
+ // Flag OFF ⇒ today's reach, byte-for-byte: none of these are binary
92
+ // media under an assets/ dir, so none of them move.
93
+ expect(listPushableAssets(designRoot)).toEqual([]);
94
+ });
95
+
96
+ test('declared canvasGroups decide what a canvas body IS', () => {
97
+ const designRoot = scratchDesignRoot();
98
+ mkdirSync(join(designRoot, 'mocks'), { recursive: true });
99
+ mkdirSync(join(designRoot, 'lib'), { recursive: true });
100
+ writeFileSync(join(designRoot, 'mocks/screen.tsx'), 'x');
101
+ writeFileSync(join(designRoot, 'lib/helpers.tsx'), 'x');
102
+ const groups = [{ path: 'mocks' }];
103
+ // `mocks/screen.tsx` is a canvas here; `lib/helpers.tsx` a shared module.
104
+ expect(listPushableAssets(designRoot, { canvasGroups: groups, syncFiles: true })).toEqual([
105
+ 'lib/helpers.tsx',
106
+ ]);
107
+ // …and both the groups and the flag reach the walk through config.json —
108
+ // the out-of-process worker's path.
109
+ writeFileSync(
110
+ join(designRoot, 'config.json'),
111
+ JSON.stringify({ canvasGroups: groups, linkedHub: { url: 'http://h', syncFiles: true } })
112
+ );
113
+ expect(listPushableAssets(designRoot)).toEqual(['lib/helpers.tsx']);
114
+ });
65
115
  });
66
116
 
67
117
  describe('pushAssets — routes each class to the right hub endpoint', () => {
@@ -714,3 +764,94 @@ describe('pushAssets — incremental progress (feature-sync-progress-modal)', ()
714
764
  expect(r.pushed).toEqual(['assets/ok.png']);
715
765
  });
716
766
  });
767
+
768
+ // An asset that appears AFTER boot has to go up now, not next launch.
769
+ //
770
+ // The sweep fired from exactly one place — `start()` — so a picture pasted into
771
+ // an annotation reached the cloud only on the next boot or Resync, while the
772
+ // annotation itself synced through the doc in milliseconds. The other side
773
+ // rendered an `<image>` pointing at bytes nobody had sent: a permanent empty
774
+ // frame that reads as a broken path. `isPushableAssetRel` is what decides an
775
+ // `fs:any` event is worth a sweep, so it has to agree with the walk that
776
+ // actually collects them.
777
+ describe('isPushableAssetRel — the on-change trigger', () => {
778
+ test('it agrees with listPushableAssets on a real tree', () => {
779
+ const root = mkdtempSync(join(tmpdir(), 'pushable-'));
780
+ mkdirSync(join(root, 'assets'), { recursive: true });
781
+ mkdirSync(join(root, 'system/ds/assets/logos'), { recursive: true });
782
+ mkdirSync(join(root, '_history/ui-home/assets'), { recursive: true });
783
+ mkdirSync(join(root, 'ui'), { recursive: true });
784
+ writeFileSync(join(root, 'assets/5f809613.png'), 'x'); // the reported case
785
+ writeFileSync(join(root, 'system/ds/assets/logos/mark.svg'), 'x');
786
+ writeFileSync(join(root, '_history/ui-home/assets/shot.png'), 'x');
787
+ writeFileSync(join(root, 'ui/Home.tsx'), 'x');
788
+ writeFileSync(join(root, 'assets/notes.txt'), 'x');
789
+
790
+ const walked = new Set(listPushableAssets(root));
791
+ for (const rel of [
792
+ 'assets/5f809613.png',
793
+ 'system/ds/assets/logos/mark.svg',
794
+ '_history/ui-home/assets/shot.png',
795
+ 'ui/Home.tsx',
796
+ 'assets/notes.txt',
797
+ ]) {
798
+ expect(isPushableAssetRel(rel)).toBe(walked.has(rel));
799
+ }
800
+ });
801
+
802
+ test('the pasted-annotation image is what fires it', () => {
803
+ expect(isPushableAssetRel('assets/5f809613.png')).toBe(true);
804
+ expect(isPushableAssetRel('assets/9fb5bab5.jpg')).toBe(true);
805
+ });
806
+
807
+ test('runtime state and non-assets never fire a sweep', () => {
808
+ for (const rel of [
809
+ '_history/ui-home/assets/shot.png',
810
+ '_comments/assets/pasted.png',
811
+ 'ui/Home.tsx',
812
+ 'config.json',
813
+ 'system/ds/preview/logo.tsx',
814
+ 'assets/notes.txt',
815
+ 'assets',
816
+ '',
817
+ ]) {
818
+ expect(isPushableAssetRel(rel)).toBe(false);
819
+ }
820
+ });
821
+
822
+ test('a windows-shaped path is normalized, not refused', () => {
823
+ expect(isPushableAssetRel('system\\ds\\assets\\logos\\mark.svg')).toBe(true);
824
+ });
825
+ });
826
+
827
+ // The WIRING, pinned at source (the sync-panel-surface precedent).
828
+ //
829
+ // `isPushableAssetRel` above is exercised directly; what a unit test cannot
830
+ // reach here is that the runtime actually CALLS it from the fs watcher, which
831
+ // is the half that was missing. A behavioural test needs a live runtime plus a
832
+ // 1.5 s debounce and destabilises `sync-runtime.test.ts`'s shared fixtures, so
833
+ // the invariant is pinned where it is cheap and unambiguous instead.
834
+ describe('the runtime sweeps on change, not only at boot', () => {
835
+ const SYNC = readFileSync(join(import.meta.dir, '..', 'sync', 'index.ts'), 'utf8');
836
+
837
+ test('the fs watcher decides with isPushableAssetRel and schedules a sweep', () => {
838
+ // The predicate gets the project's OWN canvas groups — with the default
839
+ // set a custom-group project's shared module would answer canvas-owned
840
+ // and silently never schedule the sweep that uploads it.
841
+ expect(SYNC).toContain(
842
+ 'if (isPushableAssetRel(rel, ctx.cfg.canvasGroups)) scheduleAssetSweep('
843
+ );
844
+ });
845
+
846
+ test('bursts coalesce, and a change DURING a sweep is not lost', () => {
847
+ // Dragging six images on is six events; each sweep costs a probe over the
848
+ // wire. And a file written while a sweep runs was not in that sweep's list.
849
+ expect(SYNC).toMatch(/ASSET_SWEEP_DEBOUNCE_MS = [\d_]+/);
850
+ expect(SYNC).toContain('assetSweepAgain');
851
+ });
852
+
853
+ test('only one sweep runs at a time, and stop() clears the pending one', () => {
854
+ expect(SYNC).toMatch(/if \(stopped \|\| assetSweep\) return;/);
855
+ expect(SYNC).toContain('if (assetSweepTimer !== null) clearTimeout(assetSweepTimer);');
856
+ });
857
+ });