@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.
- package/apps/studio/api.ts +14 -0
- package/apps/studio/client/app.jsx +44 -9
- package/apps/studio/client/panels/SyncPanel.jsx +53 -11
- package/apps/studio/context.ts +9 -0
- package/apps/studio/dist/client.bundle.js +525 -525
- package/apps/studio/http.ts +48 -7
- package/apps/studio/server.ts +11 -0
- package/apps/studio/sync/asset-pull.ts +210 -0
- package/apps/studio/sync/asset-push.ts +118 -75
- package/apps/studio/sync/connection-state.ts +23 -0
- package/apps/studio/sync/discovery.ts +139 -0
- package/apps/studio/sync/file-membership.ts +290 -0
- package/apps/studio/sync/file-pull.ts +330 -0
- package/apps/studio/sync/index.ts +995 -166
- package/apps/studio/sync/remote-docs.ts +98 -4
- package/apps/studio/sync/status.ts +25 -0
- package/apps/studio/sync/tombstone-apply.ts +131 -0
- package/apps/studio/test/cloud-managed-save-surfaces.test.ts +90 -0
- package/apps/studio/test/exporters/jobs.test.ts +10 -4
- package/apps/studio/test/git-cloud-posture.test.ts +11 -2
- package/apps/studio/test/sync-asset-pull.test.ts +161 -0
- package/apps/studio/test/sync-asset-push.test.ts +144 -3
- package/apps/studio/test/sync-attach-incremental.test.ts +527 -0
- package/apps/studio/test/sync-file-membership.test.ts +331 -0
- package/apps/studio/test/sync-file-pull.test.ts +333 -0
- package/apps/studio/test/sync-fresh-link-parity.test.ts +267 -0
- package/apps/studio/test/sync-panel-surface.test.ts +10 -0
- package/apps/studio/test/sync-remote-docs.test.ts +118 -8
- package/apps/studio/test/sync-resync-routes.test.ts +43 -0
- package/apps/studio/test/sync-status.test.ts +18 -0
- package/apps/studio/test/sync-supervisor.test.ts +4 -0
- package/apps/studio/test/sync-tombstone-apply.test.ts +111 -0
- package/apps/studio/test/sync-two-peer-discovery.test.ts +343 -0
- package/apps/studio/whats-new.json +18 -0
- package/cli/commands/doctor.mjs +141 -6
- package/cli/lib/gitignore-drift.mjs +149 -0
- package/cli/lib/gitignore-drift.test.mjs +156 -0
- 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 {
|
|
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 —
|
|
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
|
+
});
|