@wenrwa/marketplace-cli 0.6.6 → 0.7.0
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/README.md +85 -85
- package/dist/__tests__/auth-login-core.test.js +2 -2
- package/dist/__tests__/auth.test.js +4 -4
- package/dist/__tests__/bridge-autostart.test.js +9 -9
- package/dist/__tests__/bridge-checkout-dirty-real-git.test.d.ts +1 -0
- package/dist/__tests__/bridge-checkout-dirty-real-git.test.js +145 -0
- package/dist/__tests__/bridge-checkout-inventory-reporter.test.js +6 -2
- package/dist/__tests__/bridge-checkout-inventory.test.js +128 -14
- package/dist/__tests__/bridge-checkout-park-and-remove.test.js +57 -8
- package/dist/__tests__/bridge-claude-trust.test.d.ts +1 -0
- package/dist/__tests__/bridge-claude-trust.test.js +137 -0
- package/dist/__tests__/bridge-command-browser-options.test.js +1 -1
- package/dist/__tests__/bridge-command-install-env.test.js +1 -1
- package/dist/__tests__/bridge-device-registration.test.js +1 -1
- package/dist/__tests__/bridge-folder-registry.test.js +1 -1
- package/dist/__tests__/bridge-local-add-core.test.js +1 -1
- package/dist/__tests__/bridge-match-pointer.test.d.ts +1 -0
- package/dist/__tests__/bridge-match-pointer.test.js +136 -0
- package/dist/__tests__/bridge-pairing-api.test.js +1 -1
- package/dist/__tests__/bridge-pairing-core.test.js +1 -1
- package/dist/__tests__/bridge-park-remove-frame.test.js +61 -0
- package/dist/__tests__/bridge-relay-client-checkout-inventory.test.js +1 -1
- package/dist/__tests__/bridge-relay-mode.test.js +1 -1
- package/dist/__tests__/bridge-server.test.js +1 -1
- package/dist/__tests__/config.test.js +1 -1
- package/dist/__tests__/devices.test.js +1 -1
- package/dist/__tests__/local.test.js +1 -1
- package/dist/__tests__/openBrowser.test.js +1 -1
- package/dist/__tests__/websocket.test.js +3 -3
- package/dist/auth/identity.d.ts +1 -1
- package/dist/auth/login.d.ts +2 -2
- package/dist/bridge/apiClient.js +2 -2
- package/dist/bridge/autostart.d.ts +1 -1
- package/dist/bridge/autostart.js +1 -1
- package/dist/bridge/checkoutInventory.d.ts +48 -17
- package/dist/bridge/checkoutInventory.js +190 -44
- package/dist/bridge/checkoutParkAndRemove.d.ts +22 -1
- package/dist/bridge/checkoutParkAndRemove.js +37 -7
- package/dist/bridge/claudeTrust.d.ts +37 -0
- package/dist/bridge/claudeTrust.js +85 -0
- package/dist/bridge/config.d.ts +1 -1
- package/dist/bridge/contextWriter.d.ts +2 -0
- package/dist/bridge/contextWriter.js +3 -2
- package/dist/bridge/deviceRegistration.d.ts +1 -1
- package/dist/bridge/envSanitize.d.ts +1 -1
- package/dist/bridge/envSanitize.js +1 -1
- package/dist/bridge/folderRegistry.js +1 -1
- package/dist/bridge/localAdd.js +1 -1
- package/dist/bridge/localContextWriter.d.ts +2 -0
- package/dist/bridge/localContextWriter.js +3 -2
- package/dist/bridge/memoryBrief.d.ts +16 -4
- package/dist/bridge/memoryBrief.js +17 -9
- package/dist/bridge/pairing.d.ts +1 -1
- package/dist/bridge/relayClient.js +11 -1
- package/dist/bridge/server.d.ts +1 -1
- package/dist/bridge/workGraphPointer.d.ts +19 -0
- package/dist/bridge/workGraphPointer.js +26 -1
- package/dist/commands/apps.js +1 -1
- package/dist/commands/auth.d.ts +1 -1
- package/dist/commands/auth.js +6 -6
- package/dist/commands/bridge.js +40 -13
- package/dist/commands/credits.js +1 -1
- package/dist/commands/devices.d.ts +1 -1
- package/dist/commands/devices.js +4 -4
- package/dist/commands/local.d.ts +1 -1
- package/dist/commands/local.js +5 -5
- package/dist/commands/pair.d.ts +1 -1
- package/dist/commands/pair.js +4 -4
- package/dist/index.js +2 -2
- package/dist/utils/config.js +2 -2
- package/dist/utils/openBrowser.d.ts +1 -1
- package/dist/utils/openBrowser.js +1 -1
- package/dist/utils/websocket.js +1 -1
- package/package.json +3 -2
|
@@ -29,23 +29,40 @@ function makeDeps(opts) {
|
|
|
29
29
|
runCalls.push({ path: id, args });
|
|
30
30
|
const fixture = opts.git?.[id] ?? {};
|
|
31
31
|
const sub = args[2];
|
|
32
|
+
const fail = (msg) => {
|
|
33
|
+
if (fixture.rejectOnFailure)
|
|
34
|
+
throw new Error(`Command failed: git ${args.join(' ')}\n${msg}`);
|
|
35
|
+
return { stdout: '', code: 128 };
|
|
36
|
+
};
|
|
32
37
|
if (sub === 'status') {
|
|
33
38
|
if (fixture.failStatus)
|
|
34
|
-
return
|
|
39
|
+
return fail('fatal: not a git repository');
|
|
35
40
|
return { stdout: fixture.status ?? '', code: 0 };
|
|
36
41
|
}
|
|
37
42
|
if (sub === 'stash') {
|
|
38
43
|
if (fixture.failStash)
|
|
39
|
-
return
|
|
44
|
+
return fail('fatal: not a git repository');
|
|
40
45
|
return { stdout: fixture.stash ?? '', code: 0 };
|
|
41
46
|
}
|
|
42
47
|
if (sub === 'rev-list') {
|
|
48
|
+
expect(args.slice(3)).toEqual(['HEAD', '--not', '--remotes', '--count']);
|
|
43
49
|
if (fixture.ahead === undefined)
|
|
44
|
-
return
|
|
50
|
+
return fail("fatal: ambiguous argument 'HEAD'");
|
|
45
51
|
return { stdout: fixture.ahead, code: 0 };
|
|
46
52
|
}
|
|
53
|
+
if (sub === 'rev-parse') {
|
|
54
|
+
if (fixture.upstream === false)
|
|
55
|
+
return fail("fatal: no upstream configured for branch 'backup/x'");
|
|
56
|
+
return { stdout: 'origin/main\n', code: 0 };
|
|
57
|
+
}
|
|
58
|
+
if (sub === 'show') {
|
|
59
|
+
if (fixture.headClaudeMd === undefined)
|
|
60
|
+
return fail("fatal: path 'CLAUDE.md' does not exist in 'HEAD'");
|
|
61
|
+
return { stdout: fixture.headClaudeMd, code: 0 };
|
|
62
|
+
}
|
|
47
63
|
throw new Error(`unexpected git call: ${args.join(' ')}`);
|
|
48
64
|
},
|
|
65
|
+
readText: (p) => opts.files?.[p] ?? null,
|
|
49
66
|
};
|
|
50
67
|
return { deps, runCalls, sizeCalls };
|
|
51
68
|
}
|
|
@@ -57,7 +74,10 @@ describe('scanCheckoutInventory — dirty detection (independent conditions)', (
|
|
|
57
74
|
git: { 'ws-clean': { status: '', stash: '', ahead: '0' } },
|
|
58
75
|
});
|
|
59
76
|
const [entry] = await (0, checkoutInventory_1.scanCheckoutInventory)(BASE, deps);
|
|
60
|
-
expect(entry).toEqual({
|
|
77
|
+
expect(entry).toEqual({
|
|
78
|
+
workspaceId: 'ws-clean', unparseable: false, bytes: 4096, dirty: false, mtimeMs: 1000,
|
|
79
|
+
dirtyKinds: [], noUpstream: false, scanVersion: 2,
|
|
80
|
+
});
|
|
61
81
|
});
|
|
62
82
|
it('uncommitted changes alone mark the checkout dirty', async () => {
|
|
63
83
|
const { deps } = makeDeps({
|
|
@@ -86,15 +106,107 @@ describe('scanCheckoutInventory — dirty detection (independent conditions)', (
|
|
|
86
106
|
const [entry] = await (0, checkoutInventory_1.scanCheckoutInventory)(BASE, deps);
|
|
87
107
|
expect(entry).toMatchObject({ dirty: true, unparseable: false });
|
|
88
108
|
});
|
|
89
|
-
it('no
|
|
109
|
+
it('a repo with no commits (rev-list fails) is not unpushed, and not a parse failure', async () => {
|
|
90
110
|
const { deps } = makeDeps({
|
|
91
|
-
workspaceIds: ['ws-
|
|
92
|
-
sizes: { 'ws-
|
|
93
|
-
git: { 'ws-
|
|
111
|
+
workspaceIds: ['ws-empty'],
|
|
112
|
+
sizes: { 'ws-empty': 100 },
|
|
113
|
+
git: { 'ws-empty': { status: '', stash: '' } }, // ahead undefined -> rev-list fails
|
|
94
114
|
});
|
|
95
115
|
const [entry] = await (0, checkoutInventory_1.scanCheckoutInventory)(BASE, deps);
|
|
96
116
|
expect(entry).toMatchObject({ dirty: false, unparseable: false });
|
|
97
117
|
});
|
|
118
|
+
it('names WHICH conditions made it dirty', async () => {
|
|
119
|
+
const { deps } = makeDeps({
|
|
120
|
+
workspaceIds: ['ws'],
|
|
121
|
+
git: { ws: { status: ' M a.ts\n', stash: 'stash@{0}: x\n', ahead: '2' } },
|
|
122
|
+
files: {},
|
|
123
|
+
});
|
|
124
|
+
const [entry] = await (0, checkoutInventory_1.scanCheckoutInventory)(BASE, deps);
|
|
125
|
+
expect(entry).toMatchObject({ dirty: true, dirtyKinds: ['uncommitted', 'unpushed', 'stashed'] });
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
/*
|
|
129
|
+
Owner report 2026-10-02 — the Storage view said "Could not read this
|
|
130
|
+
checkout: Command failed: git -C … rev-list @{u}..HEAD --count fatal: no
|
|
131
|
+
upstream configured for branch 'backup/botsim-swee…'". The production runner
|
|
132
|
+
REJECTS on a failed command; the old check let that escape.
|
|
133
|
+
*/
|
|
134
|
+
describe('scanCheckoutInventory — a branch with no upstream (production runner rejects)', () => {
|
|
135
|
+
it('is a readable checkout with unpushed work and noUpstream, never unparseable', async () => {
|
|
136
|
+
const { deps } = makeDeps({
|
|
137
|
+
workspaceIds: ['ws-backup'],
|
|
138
|
+
sizes: { 'ws-backup': 100 },
|
|
139
|
+
git: { 'ws-backup': { status: '', stash: '', ahead: '3', upstream: false, rejectOnFailure: true } },
|
|
140
|
+
});
|
|
141
|
+
const [entry] = await (0, checkoutInventory_1.scanCheckoutInventory)(BASE, deps);
|
|
142
|
+
expect(entry).toMatchObject({ unparseable: false, dirty: true, dirtyKinds: ['unpushed'], noUpstream: true });
|
|
143
|
+
});
|
|
144
|
+
it('with every commit already on a remote, it is clean even without an upstream', async () => {
|
|
145
|
+
const { deps } = makeDeps({
|
|
146
|
+
workspaceIds: ['ws-gone'],
|
|
147
|
+
git: { 'ws-gone': { status: '', stash: '', ahead: '0', upstream: false, rejectOnFailure: true } },
|
|
148
|
+
});
|
|
149
|
+
const [entry] = await (0, checkoutInventory_1.scanCheckoutInventory)(BASE, deps);
|
|
150
|
+
expect(entry).toMatchObject({ unparseable: false, dirty: false, noUpstream: true });
|
|
151
|
+
});
|
|
152
|
+
it('a status that rejects is still unparseable (not a git checkout)', async () => {
|
|
153
|
+
const { deps } = makeDeps({
|
|
154
|
+
workspaceIds: ['ws-notgit'],
|
|
155
|
+
git: { 'ws-notgit': { failStatus: true, rejectOnFailure: true } },
|
|
156
|
+
});
|
|
157
|
+
const [entry] = await (0, checkoutInventory_1.scanCheckoutInventory)(BASE, deps);
|
|
158
|
+
expect(entry).toMatchObject({ unparseable: true, reason: 'git status failed — not a usable checkout' });
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
/*
|
|
162
|
+
Owner report 2026-10-02 — nearly every row, finished match copies that were
|
|
163
|
+
pushed included, said "Unsaved work". The bridge itself rewrites a managed
|
|
164
|
+
region of CLAUDE.md whenever a session opens (wmr-07).
|
|
165
|
+
*/
|
|
166
|
+
describe('scanCheckoutInventory — the bridge’s own writes are not unsaved work', () => {
|
|
167
|
+
const REGION = '<!-- HIVEOP:CONTEXT:START -->\n## Work graph\nstuff\n<!-- HIVEOP:CONTEXT:END -->\n';
|
|
168
|
+
it('an untracked CLAUDE.md holding only the managed region is clean', async () => {
|
|
169
|
+
const { deps } = makeDeps({
|
|
170
|
+
workspaceIds: ['ws'],
|
|
171
|
+
git: { ws: { status: '?? CLAUDE.md\n', stash: '', ahead: '0' } },
|
|
172
|
+
files: { [`${BASE}/ws/CLAUDE.md`]: REGION },
|
|
173
|
+
});
|
|
174
|
+
const [entry] = await (0, checkoutInventory_1.scanCheckoutInventory)(BASE, deps);
|
|
175
|
+
expect(entry).toMatchObject({ dirty: false, dirtyKinds: [] });
|
|
176
|
+
});
|
|
177
|
+
it('a committed CLAUDE.md that changed only inside the region is clean', async () => {
|
|
178
|
+
const { deps } = makeDeps({
|
|
179
|
+
workspaceIds: ['ws'],
|
|
180
|
+
git: { ws: { status: ' M CLAUDE.md\n', stash: '', ahead: '0', headClaudeMd: '# Project\n\nRules.\n' } },
|
|
181
|
+
files: { [`${BASE}/ws/CLAUDE.md`]: `# Project\n\nRules.\n\n${REGION}` },
|
|
182
|
+
});
|
|
183
|
+
const [entry] = await (0, checkoutInventory_1.scanCheckoutInventory)(BASE, deps);
|
|
184
|
+
expect(entry).toMatchObject({ dirty: false });
|
|
185
|
+
});
|
|
186
|
+
it('user text in CLAUDE.md outside the region IS unsaved work', async () => {
|
|
187
|
+
const { deps } = makeDeps({
|
|
188
|
+
workspaceIds: ['ws'],
|
|
189
|
+
git: { ws: { status: ' M CLAUDE.md\n', stash: '', ahead: '0', headClaudeMd: '# Project\n' } },
|
|
190
|
+
files: { [`${BASE}/ws/CLAUDE.md`]: `# Project\n\nMy new rule.\n${REGION}` },
|
|
191
|
+
});
|
|
192
|
+
const [entry] = await (0, checkoutInventory_1.scanCheckoutInventory)(BASE, deps);
|
|
193
|
+
expect(entry).toMatchObject({ dirty: true, dirtyKinds: ['uncommitted'] });
|
|
194
|
+
});
|
|
195
|
+
it('an untracked CLAUDE.md with user text is unsaved work', async () => {
|
|
196
|
+
const { deps } = makeDeps({
|
|
197
|
+
workspaceIds: ['ws'],
|
|
198
|
+
git: { ws: { status: '?? CLAUDE.md\n', stash: '', ahead: '0' } },
|
|
199
|
+
files: { [`${BASE}/ws/CLAUDE.md`]: `Notes\n${REGION}` },
|
|
200
|
+
});
|
|
201
|
+
const [entry] = await (0, checkoutInventory_1.scanCheckoutInventory)(BASE, deps);
|
|
202
|
+
expect(entry).toMatchObject({ dirty: true });
|
|
203
|
+
});
|
|
204
|
+
it('.hiveop/ is the bridge’s; any other file still counts', async () => {
|
|
205
|
+
const clean = makeDeps({ workspaceIds: ['ws'], git: { ws: { status: '?? .hiveop/\n', stash: '', ahead: '0' } } });
|
|
206
|
+
expect((await (0, checkoutInventory_1.scanCheckoutInventory)(BASE, clean.deps))[0]).toMatchObject({ dirty: false });
|
|
207
|
+
const dirty = makeDeps({ workspaceIds: ['ws'], git: { ws: { status: '?? .hiveop/\n?? view/index.html\n', stash: '', ahead: '0' } } });
|
|
208
|
+
expect((await (0, checkoutInventory_1.scanCheckoutInventory)(BASE, dirty.deps))[0]).toMatchObject({ dirty: true });
|
|
209
|
+
});
|
|
98
210
|
});
|
|
99
211
|
describe('scanCheckoutInventory — reporting shape', () => {
|
|
100
212
|
it('one record per checkout, with workspace id, bytes, dirty flag, and mtime', async () => {
|
|
@@ -109,8 +221,8 @@ describe('scanCheckoutInventory — reporting shape', () => {
|
|
|
109
221
|
});
|
|
110
222
|
const entries = await (0, checkoutInventory_1.scanCheckoutInventory)(BASE, deps);
|
|
111
223
|
expect(entries).toEqual([
|
|
112
|
-
{ workspaceId: 'ws-a', unparseable: false, bytes: 1000, dirty: false, mtimeMs: 111 },
|
|
113
|
-
{ workspaceId: 'ws-b', unparseable: false, bytes: 2000, dirty: true, mtimeMs: 222 },
|
|
224
|
+
{ workspaceId: 'ws-a', unparseable: false, bytes: 1000, dirty: false, mtimeMs: 111, dirtyKinds: [], noUpstream: false, scanVersion: 2 },
|
|
225
|
+
{ workspaceId: 'ws-b', unparseable: false, bytes: 2000, dirty: true, mtimeMs: 222, dirtyKinds: ['uncommitted'], noUpstream: false, scanVersion: 2 },
|
|
114
226
|
]);
|
|
115
227
|
});
|
|
116
228
|
it('an unreadable checkouts root yields an empty inventory, not a crash', async () => {
|
|
@@ -162,8 +274,8 @@ describe('scanCheckoutInventory — malformed/unreadable checkouts', () => {
|
|
|
162
274
|
expect(entries.find((e) => e.workspaceId === 'ws-fine')?.unparseable).toBe(false);
|
|
163
275
|
});
|
|
164
276
|
});
|
|
165
|
-
describe('scanCheckoutInventory — mtime cache (
|
|
166
|
-
it('reuses the cached
|
|
277
|
+
describe('scanCheckoutInventory — mtime cache (size only)', () => {
|
|
278
|
+
it('reuses the cached SIZE when the mtime has not changed, but re-checks dirty every scan', async () => {
|
|
167
279
|
const { deps, runCalls, sizeCalls } = makeDeps({
|
|
168
280
|
workspaceIds: ['ws-a'],
|
|
169
281
|
mtimes: { 'ws-a': 500 },
|
|
@@ -176,8 +288,10 @@ describe('scanCheckoutInventory — mtime cache (unchanged checkouts are not re-
|
|
|
176
288
|
expect(runCalls.length).toBeGreaterThan(0);
|
|
177
289
|
const runCallsAfterFirst = runCalls.length;
|
|
178
290
|
const second = await (0, checkoutInventory_1.scanCheckoutInventory)(BASE, deps, cache);
|
|
179
|
-
expect(sizeCalls).toEqual(['ws-a']); // not
|
|
180
|
-
|
|
291
|
+
expect(sizeCalls).toEqual(['ws-a']); // du not run again
|
|
292
|
+
// …but git is: a push lands inside .git and never moves the top-level
|
|
293
|
+
// mtime, so a cached dirty flag outlived the push that cleared it.
|
|
294
|
+
expect(runCalls.length).toBeGreaterThan(runCallsAfterFirst);
|
|
181
295
|
expect(second).toEqual(first);
|
|
182
296
|
});
|
|
183
297
|
it('rescans when the mtime has changed', async () => {
|
|
@@ -266,26 +266,43 @@ describe('checkoutParkAndRemove (hcs-06)', () => {
|
|
|
266
266
|
expect(calls.some((c) => c.args.includes('commit'))).toBe(false);
|
|
267
267
|
expect(calls.some((c) => c.args.includes('push'))).toBe(false);
|
|
268
268
|
});
|
|
269
|
-
it('nothing to commit and nothing unpushed ->
|
|
269
|
+
it('remove with nothing to commit and nothing unpushed -> removed through the reclaimer, never pushes', async () => {
|
|
270
|
+
// Everything here is already on a remote. Refusing left clean match copies
|
|
271
|
+
// with no way out but the 14-day automatic sweep; the reclaimer's own
|
|
272
|
+
// re-check (clean, unattached) still guards the delete.
|
|
270
273
|
const reclaimer = fakeReclaimer();
|
|
271
274
|
const script = (args) => {
|
|
272
275
|
if (args.includes('add'))
|
|
273
276
|
return { stdout: '', code: 0 };
|
|
274
277
|
if (args.includes('status'))
|
|
275
|
-
return { stdout: '', code: 0 };
|
|
278
|
+
return { stdout: '', code: 0 };
|
|
276
279
|
if (args.includes('rev-list'))
|
|
277
|
-
return { stdout: '0\n', code: 0 };
|
|
280
|
+
return { stdout: '0\n', code: 0 };
|
|
278
281
|
return { stdout: '', code: 0 };
|
|
279
282
|
};
|
|
280
283
|
const { deps, calls } = baseDeps(script, reclaimer);
|
|
281
|
-
const
|
|
282
|
-
|
|
283
|
-
expect(
|
|
284
|
-
expect(result.branch).toBe((0, checkoutParkAndRemove_1.parkingBranchName)(WS));
|
|
285
|
-
expect(reclaimer.calls).toEqual([]);
|
|
284
|
+
const result = await (0, checkoutParkAndRemove_1.createCheckoutParker)(deps).park(WS);
|
|
285
|
+
expect(result).toEqual({ workspaceId: WS, outcome: 'removed', branch: (0, checkoutParkAndRemove_1.parkingBranchName)(WS), bytesFreed: 4096 });
|
|
286
|
+
expect(reclaimer.calls).toHaveLength(1);
|
|
286
287
|
expect(calls.some((c) => c.args.includes('push'))).toBe(false);
|
|
287
288
|
expect(calls.some((c) => c.args.includes('commit'))).toBe(false);
|
|
288
289
|
});
|
|
290
|
+
it('remove with nothing unsaved, but the reclaimer refuses (a session attached) -> retained with its reason', async () => {
|
|
291
|
+
const reclaimer = fakeReclaimer({
|
|
292
|
+
reclaim: async (v) => v.map((x) => ({ workspaceId: x.workspaceId, outcome: 'aborted', reason: 'a session attached' })),
|
|
293
|
+
});
|
|
294
|
+
const script = (args) => args.includes('rev-list') ? { stdout: '0\n', code: 0 } : { stdout: '', code: 0 };
|
|
295
|
+
const { deps } = baseDeps(script, reclaimer);
|
|
296
|
+
const result = await (0, checkoutParkAndRemove_1.createCheckoutParker)(deps).park(WS);
|
|
297
|
+
expect(result).toMatchObject({ outcome: 'retained', phase: 'removing', reason: 'a session attached' });
|
|
298
|
+
});
|
|
299
|
+
it('unpushed is "commits no remote ref contains", not @{u}..HEAD', async () => {
|
|
300
|
+
const reclaimer = fakeReclaimer();
|
|
301
|
+
const { deps, calls } = baseDeps(defaultScript(), reclaimer);
|
|
302
|
+
await (0, checkoutParkAndRemove_1.createCheckoutParker)(deps).park(WS);
|
|
303
|
+
const revList = calls.find((c) => c.args.includes('rev-list'));
|
|
304
|
+
expect(revList.args.slice(3)).toEqual(['HEAD', '--not', '--remotes', '--count']);
|
|
305
|
+
});
|
|
289
306
|
it('nothing to commit but there ARE unpushed commits -> still pushes and removes', async () => {
|
|
290
307
|
const reclaimer = fakeReclaimer();
|
|
291
308
|
const script = (args) => {
|
|
@@ -383,4 +400,36 @@ describe('checkoutParkAndRemove (hcs-06)', () => {
|
|
|
383
400
|
expect(firstResult.outcome).toBe('removed');
|
|
384
401
|
expect(pushCalls).toBe(1); // never raced into a second push
|
|
385
402
|
});
|
|
403
|
+
describe('Save (park without remove)', () => {
|
|
404
|
+
it('commits, pushes, verifies, records the tracking ref — and keeps the checkout', async () => {
|
|
405
|
+
const reclaimer = fakeReclaimer();
|
|
406
|
+
const sha = 'b'.repeat(40);
|
|
407
|
+
const { deps, calls } = baseDeps(defaultScript(sha), reclaimer);
|
|
408
|
+
const phases = [];
|
|
409
|
+
const result = await (0, checkoutParkAndRemove_1.createCheckoutParker)(deps).park(WS, (p) => phases.push(p), { remove: false });
|
|
410
|
+
expect(result).toEqual({ workspaceId: WS, outcome: 'saved', branch: (0, checkoutParkAndRemove_1.parkingBranchName)(WS) });
|
|
411
|
+
expect(phases).toEqual(['committing', 'pushing']);
|
|
412
|
+
expect(reclaimer.calls).toEqual([]);
|
|
413
|
+
const updateRef = calls.find((c) => c.args.includes('update-ref'));
|
|
414
|
+
expect(updateRef.args.slice(2)).toEqual(['update-ref', `refs/remotes/origin/park/${WS}`, sha]);
|
|
415
|
+
});
|
|
416
|
+
it('an unverified push is never reported as saved', async () => {
|
|
417
|
+
const reclaimer = fakeReclaimer();
|
|
418
|
+
const base = defaultScript();
|
|
419
|
+
const script = (args) => (args.includes('ls-remote') ? { stdout: '', code: 0 } : base(args));
|
|
420
|
+
const { deps, calls } = baseDeps(script, reclaimer);
|
|
421
|
+
const result = await (0, checkoutParkAndRemove_1.createCheckoutParker)(deps).park(WS, undefined, { remove: false });
|
|
422
|
+
expect(result.outcome).toBe('retained');
|
|
423
|
+
expect(calls.some((c) => c.args.includes('update-ref'))).toBe(false);
|
|
424
|
+
});
|
|
425
|
+
it('nothing unsaved -> nothing-to-park, nothing pushed, nothing removed', async () => {
|
|
426
|
+
const reclaimer = fakeReclaimer();
|
|
427
|
+
const script = (args) => args.includes('rev-list') ? { stdout: '0\n', code: 0 } : { stdout: '', code: 0 };
|
|
428
|
+
const { deps, calls } = baseDeps(script, reclaimer);
|
|
429
|
+
const result = await (0, checkoutParkAndRemove_1.createCheckoutParker)(deps).park(WS, undefined, { remove: false });
|
|
430
|
+
expect(result.outcome).toBe('nothing-to-park');
|
|
431
|
+
expect(reclaimer.calls).toEqual([]);
|
|
432
|
+
expect(calls.some((c) => c.args.includes('push'))).toBe(false);
|
|
433
|
+
});
|
|
434
|
+
});
|
|
386
435
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
const fs = __importStar(require("fs"));
|
|
37
|
+
const os = __importStar(require("os"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const claudeTrust_1 = require("../bridge/claudeTrust");
|
|
40
|
+
/**
|
|
41
|
+
* Playtest 2026-10-01 (Qubit Forge, hosted lane): Claude Code 2.1.28x shows
|
|
42
|
+
* its folder-trust dialog even under --dangerously-skip-permissions, on every
|
|
43
|
+
* fresh /data/repos/<workspaceId> checkout — and its default answer is
|
|
44
|
+
* "No, exit". The container is ours and the bridge cloned the folder, so the
|
|
45
|
+
* bridge marks the checkout trusted in the container's Claude config
|
|
46
|
+
* ($CLAUDE_CONFIG_DIR/.claude.json → projects[<dir>].hasTrustDialogAccepted),
|
|
47
|
+
* the key Claude Code reads (verified against 2.1.286: the entry suppresses
|
|
48
|
+
* the dialog; a parent entry does NOT count inside a git repo — Claude only
|
|
49
|
+
* walks up to the repo root, so the entry must name the checkout itself).
|
|
50
|
+
*
|
|
51
|
+
* Real files in a temp dir: the contract is the file Claude reads.
|
|
52
|
+
*/
|
|
53
|
+
describe('trustClaudeProjectDir', () => {
|
|
54
|
+
let root;
|
|
55
|
+
let configDir;
|
|
56
|
+
let reposBaseDir;
|
|
57
|
+
const configFile = () => path.join(configDir, '.claude.json');
|
|
58
|
+
const read = () => JSON.parse(fs.readFileSync(configFile(), 'utf-8'));
|
|
59
|
+
const trust = (dir) => (0, claudeTrust_1.trustClaudeProjectDir)(dir, { configDir, reposBaseDir });
|
|
60
|
+
beforeEach(() => {
|
|
61
|
+
root = fs.mkdtempSync(path.join(os.tmpdir(), 'claude-trust-'));
|
|
62
|
+
configDir = path.join(root, '.claude');
|
|
63
|
+
reposBaseDir = path.join(root, 'repos');
|
|
64
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
65
|
+
fs.mkdirSync(path.join(reposBaseDir, 'ws-1'), { recursive: true });
|
|
66
|
+
});
|
|
67
|
+
afterEach(() => fs.rmSync(root, { recursive: true, force: true }));
|
|
68
|
+
it('marks a bridge-managed checkout trusted, keeping every other key', () => {
|
|
69
|
+
const repo = path.join(reposBaseDir, 'ws-1');
|
|
70
|
+
fs.writeFileSync(configFile(), JSON.stringify({
|
|
71
|
+
hasCompletedOnboarding: true,
|
|
72
|
+
bypassPermissionsModeAccepted: true,
|
|
73
|
+
mcpServers: { x: { command: 'y' } },
|
|
74
|
+
projects: { '/elsewhere': { allowedTools: ['Bash'], hasTrustDialogAccepted: false } },
|
|
75
|
+
}));
|
|
76
|
+
expect(trust(repo)).toBe('trusted');
|
|
77
|
+
const cfg = read();
|
|
78
|
+
expect(cfg.projects[repo].hasTrustDialogAccepted).toBe(true);
|
|
79
|
+
expect(cfg.projects['/elsewhere']).toEqual({ allowedTools: ['Bash'], hasTrustDialogAccepted: false });
|
|
80
|
+
expect(cfg.hasCompletedOnboarding).toBe(true);
|
|
81
|
+
expect(cfg.bypassPermissionsModeAccepted).toBe(true);
|
|
82
|
+
expect(cfg.mcpServers).toEqual({ x: { command: 'y' } });
|
|
83
|
+
});
|
|
84
|
+
it("keeps the project's existing entry (allowed tools, history) and only adds the flag", () => {
|
|
85
|
+
const repo = path.join(reposBaseDir, 'ws-1');
|
|
86
|
+
fs.writeFileSync(configFile(), JSON.stringify({ projects: { [repo]: { allowedTools: ['Edit'], lastCost: 1 } } }));
|
|
87
|
+
expect(trust(repo)).toBe('trusted');
|
|
88
|
+
expect(read().projects[repo]).toEqual({ allowedTools: ['Edit'], lastCost: 1, hasTrustDialogAccepted: true });
|
|
89
|
+
});
|
|
90
|
+
it('is idempotent: an already-trusted checkout is not rewritten', () => {
|
|
91
|
+
const repo = path.join(reposBaseDir, 'ws-1');
|
|
92
|
+
const raw = JSON.stringify({ projects: { [repo]: { hasTrustDialogAccepted: true } } });
|
|
93
|
+
fs.writeFileSync(configFile(), raw);
|
|
94
|
+
expect(trust(repo)).toBe('already');
|
|
95
|
+
expect(fs.readFileSync(configFile(), 'utf-8')).toBe(raw);
|
|
96
|
+
});
|
|
97
|
+
it('creates the config when Claude has not written one yet', () => {
|
|
98
|
+
const repo = path.join(reposBaseDir, 'ws-1');
|
|
99
|
+
expect(trust(repo)).toBe('trusted');
|
|
100
|
+
expect(read()).toEqual({ projects: { [repo]: { hasTrustDialogAccepted: true } } });
|
|
101
|
+
});
|
|
102
|
+
it('never clobbers a config it cannot parse (that file holds the login state too)', () => {
|
|
103
|
+
const repo = path.join(reposBaseDir, 'ws-1');
|
|
104
|
+
fs.writeFileSync(configFile(), '{ half-written');
|
|
105
|
+
expect(trust(repo)).toBe('skipped');
|
|
106
|
+
expect(fs.readFileSync(configFile(), 'utf-8')).toBe('{ half-written');
|
|
107
|
+
});
|
|
108
|
+
it('only trusts folders the bridge manages: never the repos root itself, never outside it', () => {
|
|
109
|
+
expect(trust(reposBaseDir)).toBe('skipped');
|
|
110
|
+
expect(trust(path.join(root, 'home', 'project'))).toBe('skipped');
|
|
111
|
+
expect(trust(path.join(reposBaseDir, '..', 'escape'))).toBe('skipped');
|
|
112
|
+
expect(trust(`${reposBaseDir}-sibling/ws`)).toBe('skipped');
|
|
113
|
+
expect(fs.existsSync(configFile())).toBe(false);
|
|
114
|
+
});
|
|
115
|
+
it('leaves no temp file behind', () => {
|
|
116
|
+
trust(path.join(reposBaseDir, 'ws-1'));
|
|
117
|
+
expect(fs.readdirSync(configDir)).toEqual(['.claude.json']);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
/**
|
|
121
|
+
* Wiring: the HOSTED bridge's clone-on-demand path (resolveWorkspaceCwd in
|
|
122
|
+
* commands/bridge.ts) pre-trusts the checkout, gated on CLAUDE_CONFIG_DIR.
|
|
123
|
+
* Code only (comments stripped) so prose cannot satisfy it.
|
|
124
|
+
*/
|
|
125
|
+
describe('hosted bridge wiring', () => {
|
|
126
|
+
const code = fs
|
|
127
|
+
.readFileSync(path.join(__dirname, '..', 'commands', 'bridge.ts'), 'utf-8')
|
|
128
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
129
|
+
.replace(/^\s*\/\/.*$/gm, '');
|
|
130
|
+
it('resolveWorkspaceCwd trusts the checkout it returns, only with CLAUDE_CONFIG_DIR set', () => {
|
|
131
|
+
const body = code.match(/resolveWorkspaceCwd: async[\s\S]*?return result\.path \?\? null;/)?.[0] ?? '';
|
|
132
|
+
expect(body).toMatch(/if \(result\.path && process\.env\.CLAUDE_CONFIG_DIR\)[\s\S]*?trustClaudeProjectDir\(result\.path, \{ configDir: process\.env\.CLAUDE_CONFIG_DIR, reposBaseDir \}\)/);
|
|
133
|
+
});
|
|
134
|
+
it('is wired exactly once — never into the local BYO bridge', () => {
|
|
135
|
+
expect(code.match(/trustClaudeProjectDir\(/g)).toHaveLength(1);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
/**
|
|
4
|
-
* cbs-07 — pure resolver for the Chromium options the `
|
|
4
|
+
* cbs-07 — pure resolver for the Chromium options the `hiveop bridge`
|
|
5
5
|
* (relay mode) command wires into `createBrowserDriver`. Mirrors
|
|
6
6
|
* resolveRelayMode's existing pattern in bridge.ts: the command shell itself
|
|
7
7
|
* is thin/untested, so the actual decision logic lives in a pure,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
/**
|
|
4
4
|
* Autostart PATH/command fix (2026-08-13, live bug) — pure resolver for
|
|
5
|
-
* `
|
|
5
|
+
* `hiveop bridge install`. Mirrors buildBrowserDriverOptions'/
|
|
6
6
|
* resolveRelayMode's existing pattern in bridge.ts: the actual decision
|
|
7
7
|
* (which PATH, which command) lives in a pure, inputs-in/options-out
|
|
8
8
|
* helper, unit-tested directly instead of forcing a harness onto the
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
/**
|
|
4
4
|
* TDD — lws-03: bridge/deviceRegistration.ts. "Connect this machine" — on
|
|
5
|
-
* first `
|
|
5
|
+
* first `hiveop bridge` run with a Connect Token configured, auto-
|
|
6
6
|
* register the device; idempotent thereafter (persisted deviceId short-
|
|
7
7
|
* circuits re-registration).
|
|
8
8
|
*/
|
|
@@ -36,7 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
/**
|
|
37
37
|
* TDD — lws-03: bridge/folderRegistry.ts. Persisted path→workspaceId map
|
|
38
38
|
* (`~/.wen-market/local-workspaces.json`), survives daemon restart, backs
|
|
39
|
-
* `
|
|
39
|
+
* `hiveop local add/list/remove` and the discovery advertisement.
|
|
40
40
|
*/
|
|
41
41
|
const fs = __importStar(require("fs"));
|
|
42
42
|
const os = __importStar(require("os"));
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
/**
|
|
4
4
|
* TDD — lws-05: shared idempotent-add core behind POST /local/add (the
|
|
5
|
-
* bridge-side twin of `
|
|
5
|
+
* bridge-side twin of `hiveop local add`, PRD D3). Deps-injected — no
|
|
6
6
|
* real fs/network — server.ts wires the real folderRegistry + apiClient.
|
|
7
7
|
*/
|
|
8
8
|
const localAdd_1 = require("../bridge/localAdd");
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
/**
|
|
37
|
+
* Qubit Forge playtest 2026-10-02 — in a MATCH workspace an agent said
|
|
38
|
+
* "CLAUDE.md says work should go through the HiveOp queue" and went looking
|
|
39
|
+
* for issues. A match has no queue to work: missions are scored when the
|
|
40
|
+
* pilot presses Submit on the committed, pushed code. The bridge learns a
|
|
41
|
+
* workspace is a match from the memory brief reply (`match: true`) and writes
|
|
42
|
+
* match guidance IN PLACE of the work-queue pointer, on both lanes. Ordinary
|
|
43
|
+
* workspaces are byte-identical to before.
|
|
44
|
+
*/
|
|
45
|
+
const fs = __importStar(require("fs"));
|
|
46
|
+
const os = __importStar(require("os"));
|
|
47
|
+
const path = __importStar(require("path"));
|
|
48
|
+
const contextWriter_1 = require("../bridge/contextWriter");
|
|
49
|
+
const localContextWriter_1 = require("../bridge/localContextWriter");
|
|
50
|
+
const workGraphPointer_1 = require("../bridge/workGraphPointer");
|
|
51
|
+
const memoryBrief_1 = require("../bridge/memoryBrief");
|
|
52
|
+
const fakeFs = () => {
|
|
53
|
+
const files = new Map();
|
|
54
|
+
return {
|
|
55
|
+
files,
|
|
56
|
+
deps: {
|
|
57
|
+
reposBaseDir: '/data/repos',
|
|
58
|
+
exists: (p) => files.has(p),
|
|
59
|
+
readFile: (p) => files.get(p) ?? '',
|
|
60
|
+
writeFile: (p, c) => { files.set(p, c); },
|
|
61
|
+
mkdirp: () => { },
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
describe('MATCH_POINTER', () => {
|
|
66
|
+
it('says how a match is scored and what not to do — the runtime value', () => {
|
|
67
|
+
expect(workGraphPointer_1.MATCH_POINTER).toMatch(/^## Match\n/);
|
|
68
|
+
expect(workGraphPointer_1.MATCH_POINTER).toMatch(/scored when the pilot presses Submit/);
|
|
69
|
+
expect(workGraphPointer_1.MATCH_POINTER).toMatch(/[Cc]ommit and push/);
|
|
70
|
+
expect(workGraphPointer_1.MATCH_POINTER).toMatch(/list_ready_issues/);
|
|
71
|
+
expect(workGraphPointer_1.MATCH_POINTER).toMatch(/do not use/i);
|
|
72
|
+
expect(workGraphPointer_1.MATCH_POINTER).not.toMatch(/claim_issue` before starting/);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
describe('hosted lane — writeContext', () => {
|
|
76
|
+
it('a match workspace gets the match guidance and NOT the work-queue pointer', () => {
|
|
77
|
+
const { files, deps } = fakeFs();
|
|
78
|
+
(0, contextWriter_1.writeContext)({ workspaceId: 'ws-m', pointersOnly: true, workGraphAvailable: true, match: true }, deps);
|
|
79
|
+
const out = files.get('/data/repos/ws-m/CLAUDE.md') ?? '';
|
|
80
|
+
expect(out).toContain(workGraphPointer_1.MATCH_POINTER);
|
|
81
|
+
expect(out).not.toContain(workGraphPointer_1.WORK_GRAPH_POINTER);
|
|
82
|
+
});
|
|
83
|
+
it('an ordinary workspace is unchanged', () => {
|
|
84
|
+
const { files, deps } = fakeFs();
|
|
85
|
+
(0, contextWriter_1.writeContext)({ workspaceId: 'ws-n', pointersOnly: true, workGraphAvailable: true }, deps);
|
|
86
|
+
const out = files.get('/data/repos/ws-n/CLAUDE.md') ?? '';
|
|
87
|
+
expect(out).toContain(workGraphPointer_1.WORK_GRAPH_POINTER);
|
|
88
|
+
expect(out).not.toContain(workGraphPointer_1.MATCH_POINTER);
|
|
89
|
+
});
|
|
90
|
+
it('a workspace that was a match and is kept loses the match guidance on the next write', () => {
|
|
91
|
+
const { files, deps } = fakeFs();
|
|
92
|
+
(0, contextWriter_1.writeContext)({ workspaceId: 'ws-k', pointersOnly: true, workGraphAvailable: true, match: true }, deps);
|
|
93
|
+
(0, contextWriter_1.writeContext)({ workspaceId: 'ws-k', pointersOnly: true, workGraphAvailable: true }, deps);
|
|
94
|
+
const out = files.get('/data/repos/ws-k/CLAUDE.md') ?? '';
|
|
95
|
+
expect(out).not.toContain(workGraphPointer_1.MATCH_POINTER);
|
|
96
|
+
expect(out).toContain(workGraphPointer_1.WORK_GRAPH_POINTER);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
describe('local lane — writeLocalContext', () => {
|
|
100
|
+
it('a match workspace gets the match guidance and NOT the work-queue pointer', () => {
|
|
101
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'match-ptr-'));
|
|
102
|
+
try {
|
|
103
|
+
(0, localContextWriter_1.writeLocalContext)({ projectRoot: root, pointersOnly: true, workGraphAvailable: true, match: true }, {
|
|
104
|
+
exists: (p) => fs.existsSync(p),
|
|
105
|
+
readFile: (p) => fs.readFileSync(p, 'utf8'),
|
|
106
|
+
writeFileAtomic: (p, c) => fs.writeFileSync(p, c),
|
|
107
|
+
mkdirp: (d) => fs.mkdirSync(d, { recursive: true }),
|
|
108
|
+
remove: (p) => fs.rmSync(p, { force: true }),
|
|
109
|
+
});
|
|
110
|
+
const out = fs.readFileSync(path.join(root, localContextWriter_1.LOCAL_CONTEXT_RELATIVE_PATH), 'utf8');
|
|
111
|
+
expect(out).toContain(workGraphPointer_1.MATCH_POINTER);
|
|
112
|
+
expect(out).not.toContain(workGraphPointer_1.WORK_GRAPH_POINTER);
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
describe('fetchWorkspaceBrief — the match flag rides the brief reply', () => {
|
|
120
|
+
const ENV = { ...process.env };
|
|
121
|
+
beforeEach(() => { process.env.WENRWA_CONNECT_TOKEN = 'wm_ct_' + 'a'.repeat(32); });
|
|
122
|
+
afterEach(() => { process.env = { ...ENV }; });
|
|
123
|
+
const reply = (body) => async () => ({ ok: true, status: 200, json: async () => body });
|
|
124
|
+
it('match: true → match', async () => {
|
|
125
|
+
await expect((0, memoryBrief_1.fetchWorkspaceBrief)('ws', { fetchImpl: reply({ success: true, brief: 'B', match: true }) }))
|
|
126
|
+
.resolves.toEqual({ brief: 'B', match: true });
|
|
127
|
+
});
|
|
128
|
+
it('no flag → not a match; an empty brief is still a reply', async () => {
|
|
129
|
+
await expect((0, memoryBrief_1.fetchWorkspaceBrief)('ws', { fetchImpl: reply({ success: true, brief: '' }) }))
|
|
130
|
+
.resolves.toEqual({ brief: null, match: false });
|
|
131
|
+
});
|
|
132
|
+
it('any failure → null (unknown), and fetchMemoryBrief keeps its contract', async () => {
|
|
133
|
+
await expect((0, memoryBrief_1.fetchWorkspaceBrief)('ws', { fetchImpl: async () => { throw new Error('down'); } })).resolves.toBeNull();
|
|
134
|
+
await expect((0, memoryBrief_1.fetchMemoryBrief)('ws', { fetchImpl: reply({ success: true, brief: 'B', match: true }) })).resolves.toBe('B');
|
|
135
|
+
});
|
|
136
|
+
});
|
|
@@ -52,7 +52,7 @@ describe('bridge apiClient — pairing', () => {
|
|
|
52
52
|
catch (err) {
|
|
53
53
|
expect(err).toBeInstanceOf(apiClient_1.BridgeApiError);
|
|
54
54
|
expect(err.code).toBe('PAT_FORBIDDEN');
|
|
55
|
-
expect(err.message).toBe('your pairing is out of date — run `
|
|
55
|
+
expect(err.message).toBe('your pairing is out of date — run `hiveop pair` again');
|
|
56
56
|
expect(err.message).not.toContain('Allowed:');
|
|
57
57
|
}
|
|
58
58
|
});
|
|
@@ -4,7 +4,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
4
4
|
* TDD — lws-14: bridge/pairing.ts pairCore. Orchestrates request → poll →
|
|
5
5
|
* config write as a pure core (network + fs both injected), mirroring
|
|
6
6
|
* addLocalWorkspaceCore / ensureDeviceRegistered's "pure core, thin command
|
|
7
|
-
* shell" shape so `
|
|
7
|
+
* shell" shape so `hiveop pair` itself stays a thin wrapper.
|
|
8
8
|
*/
|
|
9
9
|
const pairing_1 = require("../bridge/pairing");
|
|
10
10
|
describe('pairCore', () => {
|