amalgm 0.1.262 → 0.1.263
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/package.json +1 -1
- package/runtime/scripts/amalgm-mcp/observer/README.md +4 -1
- package/runtime/scripts/amalgm-mcp/observer/coverage.js +52 -0
- package/runtime/scripts/amalgm-mcp/observer/index.js +171 -37
- package/runtime/scripts/amalgm-mcp/observer/watch.js +6 -2
- package/runtime/scripts/amalgm-mcp/registration/entity-cloud.js +54 -19
- package/runtime/scripts/amalgm-mcp/registration/index.js +58 -12
- package/runtime/scripts/amalgm-mcp/registration/repo-followers.js +43 -4
- package/runtime/scripts/amalgm-mcp/registration/repo-states.js +15 -5
- package/runtime/scripts/amalgm-mcp/registration/service.js +99 -12
- package/runtime/scripts/amalgm-mcp/registration/tree.js +104 -20
- package/runtime/scripts/amalgm-mcp/repocard/capture.js +45 -3
- package/runtime/scripts/amalgm-mcp/repocard/follow.js +65 -10
- package/runtime/scripts/amalgm-mcp/tests/entity-cloud.test.js +90 -0
- package/runtime/scripts/amalgm-mcp/tests/entity-materialization.test.js +236 -0
- package/runtime/scripts/amalgm-mcp/tests/observer.coverage.test.js +197 -0
- package/runtime/scripts/amalgm-mcp/tests/registration.service.test.js +9 -3
- package/runtime/scripts/amalgm-mcp/tests/registration.test.js +10 -6
- package/runtime/scripts/amalgm-mcp/tests/repocard.scope.test.js +182 -0
|
@@ -770,15 +770,21 @@ test('register registers every entity before it starts Watch', async () => {
|
|
|
770
770
|
identity.close();
|
|
771
771
|
assert.ok(nested, 'the folder already has a durable identity');
|
|
772
772
|
assert.ok(file, 'so does the file within it');
|
|
773
|
-
|
|
774
|
-
|
|
773
|
+
// Watcher FIRST, conclusions LAST: by the time identity commits, the
|
|
774
|
+
// doorbell is already alive (it was proven before the walk began) —
|
|
775
|
+
// but HELD, so nothing has been observed or concluded yet. start()
|
|
776
|
+
// releasing the hold is the first moment Watch may speak.
|
|
777
|
+
assert.equal(service.observer.status()[0].watching, true,
|
|
778
|
+
'the doorbell was born and proven before identity committed');
|
|
779
|
+
assert.equal(service.observer.filesForRoot(rootId).length, 0,
|
|
780
|
+
'and held: no observation concluded while registration was in progress');
|
|
775
781
|
checked = true;
|
|
776
782
|
return start(rootId);
|
|
777
783
|
};
|
|
778
784
|
|
|
779
785
|
const { rootPath } = await service.register(ground);
|
|
780
786
|
assert.equal(rootPath, ground);
|
|
781
|
-
assert.equal(checked, true, '
|
|
787
|
+
assert.equal(checked, true, 'the held doorbell releases only after the structural walk commits');
|
|
782
788
|
});
|
|
783
789
|
|
|
784
790
|
test('register never reserves or mutates a user filename for watching', async () => {
|
|
@@ -911,13 +911,13 @@ test('add is one operation: a bare file registers, covered ground returns its tr
|
|
|
911
911
|
'ground that would swallow a registered root refuses loudly');
|
|
912
912
|
});
|
|
913
913
|
|
|
914
|
-
test('add
|
|
914
|
+
test('add refuses before identity commits when a required watcher cannot be born', () => {
|
|
915
915
|
const dir = makeDir('degraded-add');
|
|
916
916
|
const observer = createObserver({
|
|
917
917
|
db: new Database(':memory:'),
|
|
918
918
|
emit: () => {},
|
|
919
919
|
shouldEnroll: () => true,
|
|
920
|
-
watch: () => ({ degraded: true, close: () => {} }),
|
|
920
|
+
watch: () => ({ degraded: true, reason: 'ENOSPC', close: () => {} }),
|
|
921
921
|
});
|
|
922
922
|
const identityDb = new Database(':memory:');
|
|
923
923
|
const registration = createRegistration({
|
|
@@ -926,10 +926,14 @@ test('add never claims success when a required watcher cannot open', () => {
|
|
|
926
926
|
classify: () => 'file.text',
|
|
927
927
|
});
|
|
928
928
|
try {
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
929
|
+
// Registration is synchronous, never optimistic: the watcher is proven
|
|
930
|
+
// FIRST, so a doorbell that cannot be born refuses with its reason and
|
|
931
|
+
// the remedy — and leaves NOTHING behind. No half-registered ghost, no
|
|
932
|
+
// local record the cloud never heard of.
|
|
933
|
+
assert.throws(() => registration.add(dir), /watcher could not be born \(ENOSPC\).*fs\.inotify\.max_user_watches/,
|
|
934
|
+
'the refusal names the kernel budget and the fix');
|
|
935
|
+
assert.ok(!registration.tree(dir)?.root(), 'no identity row survives the refusal');
|
|
936
|
+
assert.equal(observer.listRoots().length, 0, 'no observer root survives it either — zero residue');
|
|
933
937
|
} finally {
|
|
934
938
|
registration.stop();
|
|
935
939
|
observer.stop();
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The warm checkpoint under exam: compute the state once, then let the
|
|
5
|
+
* doorbell keep it fresh — silence proves the written answer is still
|
|
6
|
+
* true, and a ding re-reads exactly what it named.
|
|
7
|
+
*
|
|
8
|
+
* The identity law above everything: a SCOPED capture's ids are
|
|
9
|
+
* byte-identical to a from-scratch capture's over the same ground.
|
|
10
|
+
* Scope changes what is READ, never what is TRUE.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const assert = require('node:assert/strict');
|
|
14
|
+
const { execFileSync } = require('child_process');
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const os = require('os');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
const test = require('node:test');
|
|
19
|
+
|
|
20
|
+
const { capture } = require('../repocard/capture');
|
|
21
|
+
const { createFollower } = require('../repocard/follow');
|
|
22
|
+
|
|
23
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'amalgm-repocard-scope-'));
|
|
24
|
+
test.after(() => fs.rmSync(tempRoot, { recursive: true, force: true }));
|
|
25
|
+
|
|
26
|
+
function repo(name) {
|
|
27
|
+
const dir = fs.realpathSync(fs.mkdtempSync(path.join(tempRoot, `${name}-`)));
|
|
28
|
+
execFileSync('git', ['init', '-q'], { cwd: dir });
|
|
29
|
+
execFileSync('git', ['config', 'user.email', 'scope@amalgm.test'], { cwd: dir });
|
|
30
|
+
execFileSync('git', ['config', 'user.name', 'Scope'], { cwd: dir });
|
|
31
|
+
execFileSync('git', ['config', 'commit.gpgsign', 'false'], { cwd: dir });
|
|
32
|
+
return dir;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const previousOf = (snap) => snap.checkpoint.untracked.map(
|
|
36
|
+
({ path: rel, mode, sha256 }) => ({ path: rel, mode, sha256 }),
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
test('a scoped capture is byte-identical to a from-scratch one — and reads less', () => {
|
|
40
|
+
const dir = repo('identity');
|
|
41
|
+
fs.writeFileSync(path.join(dir, 'kept.txt'), 'unchanged bytes');
|
|
42
|
+
fs.mkdirSync(path.join(dir, 'src'));
|
|
43
|
+
fs.writeFileSync(path.join(dir, 'src', 'edited.js'), 'v1');
|
|
44
|
+
|
|
45
|
+
const full = capture(dir, { withBundle: false });
|
|
46
|
+
fs.writeFileSync(path.join(dir, 'src', 'edited.js'), 'v2');
|
|
47
|
+
|
|
48
|
+
const scoped = capture(dir, {
|
|
49
|
+
withBundle: false,
|
|
50
|
+
suspects: ['src/edited.js'],
|
|
51
|
+
previousUntracked: previousOf(full),
|
|
52
|
+
});
|
|
53
|
+
const fresh = capture(dir, { withBundle: false });
|
|
54
|
+
|
|
55
|
+
assert.equal(scoped.stateId, fresh.stateId, 'scope changes what is read, never what is true');
|
|
56
|
+
assert.equal(scoped.checkpointId, fresh.checkpointId);
|
|
57
|
+
const kept = scoped.checkpoint.untracked.find((file) => file.path === 'kept.txt');
|
|
58
|
+
assert.equal(kept.reused, true, 'the unsuspected file was never opened');
|
|
59
|
+
assert.equal(kept.content, null);
|
|
60
|
+
assert.equal(kept.sha256, fresh.checkpoint.untracked.find((file) => file.path === 'kept.txt').sha256);
|
|
61
|
+
const edited = scoped.checkpoint.untracked.find((file) => file.path === 'src/edited.js');
|
|
62
|
+
assert.ok(!edited.reused, 'the suspect was read fresh');
|
|
63
|
+
assert.equal(edited.content.toString(), 'v2');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('a directory-named suspect covers every file beneath it', () => {
|
|
67
|
+
const dir = repo('dir-suspect');
|
|
68
|
+
fs.mkdirSync(path.join(dir, 'notes'));
|
|
69
|
+
fs.writeFileSync(path.join(dir, 'notes', 'a.md'), 'a1');
|
|
70
|
+
fs.writeFileSync(path.join(dir, 'other.md'), 'other');
|
|
71
|
+
const full = capture(dir, { withBundle: false });
|
|
72
|
+
|
|
73
|
+
fs.writeFileSync(path.join(dir, 'notes', 'a.md'), 'a2');
|
|
74
|
+
const scoped = capture(dir, {
|
|
75
|
+
withBundle: false, suspects: ['notes'], previousUntracked: previousOf(full),
|
|
76
|
+
});
|
|
77
|
+
assert.equal(scoped.stateId, capture(dir, { withBundle: false }).stateId,
|
|
78
|
+
'the OS may name only the folder; everything under it is suspect');
|
|
79
|
+
assert.ok(!scoped.checkpoint.untracked.find((file) => file.path === 'notes/a.md').reused);
|
|
80
|
+
assert.equal(scoped.checkpoint.untracked.find((file) => file.path === 'other.md').reused, true);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('new and deleted untracked files are truth regardless of scope', () => {
|
|
84
|
+
const dir = repo('churn');
|
|
85
|
+
fs.writeFileSync(path.join(dir, 'stays.txt'), 'stays');
|
|
86
|
+
fs.writeFileSync(path.join(dir, 'leaves.txt'), 'leaves');
|
|
87
|
+
const full = capture(dir, { withBundle: false });
|
|
88
|
+
|
|
89
|
+
fs.rmSync(path.join(dir, 'leaves.txt'));
|
|
90
|
+
fs.writeFileSync(path.join(dir, 'arrives.txt'), 'new');
|
|
91
|
+
const scoped = capture(dir, {
|
|
92
|
+
withBundle: false, suspects: [], previousUntracked: previousOf(full),
|
|
93
|
+
});
|
|
94
|
+
assert.equal(scoped.stateId, capture(dir, { withBundle: false }).stateId,
|
|
95
|
+
'git names the living; the previous list never resurrects the dead');
|
|
96
|
+
const paths = scoped.checkpoint.untracked.map((file) => file.path);
|
|
97
|
+
assert.ok(paths.includes('arrives.txt') && !paths.includes('leaves.txt'));
|
|
98
|
+
assert.ok(!scoped.checkpoint.untracked.find((file) => file.path === 'arrives.txt').reused,
|
|
99
|
+
'a path the previous state never held is read, scoped or not');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('bundle captures never reuse: cloud cargo reads every byte', () => {
|
|
103
|
+
const dir = repo('bundle');
|
|
104
|
+
fs.writeFileSync(path.join(dir, 'file.txt'), 'bytes');
|
|
105
|
+
fs.writeFileSync(path.join(dir, 'seed.txt'), 'seed');
|
|
106
|
+
execFileSync('git', ['add', 'seed.txt'], { cwd: dir });
|
|
107
|
+
execFileSync('git', ['commit', '-qm', 'seed'], { cwd: dir });
|
|
108
|
+
const full = capture(dir, { withBundle: false });
|
|
109
|
+
|
|
110
|
+
const bundled = capture(dir, {
|
|
111
|
+
withBundle: true, suspects: [], previousUntracked: previousOf(full),
|
|
112
|
+
});
|
|
113
|
+
const entry = bundled.checkpoint.untracked.find((file) => file.path === 'file.txt');
|
|
114
|
+
assert.ok(!entry.reused && entry.content, 'withBundle ignores scope by contract');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('the follower scopes doorbell-driven pairs and re-suspects everything on flush', async () => {
|
|
118
|
+
const dir = repo('follower');
|
|
119
|
+
fs.writeFileSync(path.join(dir, 'a.txt'), 'a');
|
|
120
|
+
fs.writeFileSync(path.join(dir, 'b.txt'), 'b');
|
|
121
|
+
|
|
122
|
+
const seen = [];
|
|
123
|
+
const follower = createFollower({
|
|
124
|
+
repoPath: dir,
|
|
125
|
+
settleMs: 10,
|
|
126
|
+
emit: () => {},
|
|
127
|
+
captureFn: (repo, { suspects } = {}) => {
|
|
128
|
+
seen.push(suspects);
|
|
129
|
+
return capture(repo, { withBundle: false, suspects });
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
await follower.start();
|
|
134
|
+
assert.equal(seen[0], null, 'birth suspects everything — rings before birth were nobody\'s');
|
|
135
|
+
assert.equal(seen[1], null, 'both halves of the pair read the same scope');
|
|
136
|
+
|
|
137
|
+
seen.length = 0;
|
|
138
|
+
fs.writeFileSync(path.join(dir, 'a.txt'), 'a2');
|
|
139
|
+
follower.ring('a.txt');
|
|
140
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
141
|
+
assert.deepEqual(seen, [['a.txt'], ['a.txt']],
|
|
142
|
+
'a settled state narrows the next pair to exactly what rang');
|
|
143
|
+
|
|
144
|
+
seen.length = 0;
|
|
145
|
+
follower.ring('a.txt');
|
|
146
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
147
|
+
assert.deepEqual(seen, [['a.txt'], ['a.txt']],
|
|
148
|
+
'consumed hints stay consumed: only the new ring is suspect');
|
|
149
|
+
|
|
150
|
+
seen.length = 0;
|
|
151
|
+
await follower.flush();
|
|
152
|
+
assert.deepEqual(seen, [null, null], 'flush exists to catch lost doorbells: it never trusts the scope');
|
|
153
|
+
|
|
154
|
+
follower.close();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('a nameless ring makes everything suspect until a pair settles', async () => {
|
|
158
|
+
const dir = repo('nameless');
|
|
159
|
+
fs.writeFileSync(path.join(dir, 'x.txt'), 'x');
|
|
160
|
+
const seen = [];
|
|
161
|
+
const follower = createFollower({
|
|
162
|
+
repoPath: dir,
|
|
163
|
+
settleMs: 10,
|
|
164
|
+
emit: () => {},
|
|
165
|
+
captureFn: (repo, { suspects } = {}) => {
|
|
166
|
+
seen.push(suspects);
|
|
167
|
+
return capture(repo, { withBundle: false, suspects });
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
await follower.start();
|
|
171
|
+
seen.length = 0;
|
|
172
|
+
|
|
173
|
+
follower.ring(null);
|
|
174
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
175
|
+
assert.deepEqual(seen, [null, null], 'null means anywhere: the whole ground is suspect');
|
|
176
|
+
|
|
177
|
+
seen.length = 0;
|
|
178
|
+
follower.ring('x.txt');
|
|
179
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
180
|
+
assert.deepEqual(seen, [['x.txt'], ['x.txt']], 'and the settle consumed the nameless suspicion');
|
|
181
|
+
follower.close();
|
|
182
|
+
});
|