@wenrwa/marketplace-cli 0.5.13-hcs.0 → 0.5.23
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/dist/__tests__/bridge-agent-launch.test.js +75 -0
- package/dist/__tests__/bridge-checkout-park-and-remove.test.d.ts +1 -0
- package/dist/__tests__/bridge-checkout-park-and-remove.test.js +386 -0
- package/dist/__tests__/bridge-checkout-reclaim.test.js +4 -2
- package/dist/__tests__/bridge-companion-forwarder-env-options.test.d.ts +1 -0
- package/dist/__tests__/bridge-companion-forwarder-env-options.test.js +46 -0
- package/dist/__tests__/bridge-companion-forwarder.test.d.ts +1 -0
- package/dist/__tests__/bridge-companion-forwarder.test.js +320 -0
- package/dist/__tests__/bridge-companion-frames.test.d.ts +1 -0
- package/dist/__tests__/bridge-companion-frames.test.js +69 -0
- package/dist/__tests__/bridge-companion-relay.test.d.ts +1 -0
- package/dist/__tests__/bridge-companion-relay.test.js +224 -0
- package/dist/__tests__/bridge-context-writer.test.js +33 -0
- package/dist/__tests__/bridge-disk-preflight.test.d.ts +1 -0
- package/dist/__tests__/bridge-disk-preflight.test.js +84 -0
- package/dist/__tests__/bridge-ensure-checkout-frame.test.d.ts +1 -0
- package/dist/__tests__/bridge-ensure-checkout-frame.test.js +280 -0
- package/dist/__tests__/bridge-listening-ports.test.d.ts +1 -0
- package/dist/__tests__/bridge-listening-ports.test.js +68 -0
- package/dist/__tests__/bridge-local-context-writer.test.js +32 -0
- package/dist/__tests__/bridge-park-remove-frame.test.d.ts +1 -0
- package/dist/__tests__/bridge-park-remove-frame.test.js +352 -0
- package/dist/__tests__/bridge-read-transcript-frame.test.d.ts +1 -0
- package/dist/__tests__/bridge-read-transcript-frame.test.js +136 -0
- package/dist/__tests__/bridge-relay-restore-open.test.d.ts +1 -0
- package/dist/__tests__/bridge-relay-restore-open.test.js +171 -0
- package/dist/__tests__/bridge-repo-size-estimate.test.d.ts +1 -0
- package/dist/__tests__/bridge-repo-size-estimate.test.js +132 -0
- package/dist/__tests__/bridge-server-local-context.test.js +8 -1
- package/dist/__tests__/bridge-server.test.js +163 -7
- package/dist/__tests__/bridge-storage-quota-fetch.test.d.ts +1 -0
- package/dist/__tests__/bridge-storage-quota-fetch.test.js +110 -0
- package/dist/__tests__/bridge-storage-quota-preflight.test.d.ts +1 -0
- package/dist/__tests__/bridge-storage-quota-preflight.test.js +72 -0
- package/dist/__tests__/bridge-work-graph-pointer.test.d.ts +1 -0
- package/dist/__tests__/bridge-work-graph-pointer.test.js +37 -0
- package/dist/__tests__/bridge-workspace-multirepo.test.js +12 -3
- package/dist/__tests__/bridge-workspace-repo-preflight.test.d.ts +1 -0
- package/dist/__tests__/bridge-workspace-repo-preflight.test.js +271 -0
- package/dist/__tests__/bridge-workspace-repo-quota-preflight.test.d.ts +1 -0
- package/dist/__tests__/bridge-workspace-repo-quota-preflight.test.js +131 -0
- package/dist/__tests__/checkout-follows-the-repo.test.d.ts +1 -0
- package/dist/__tests__/checkout-follows-the-repo.test.js +124 -0
- package/dist/__tests__/claude-session-history.test.d.ts +1 -0
- package/dist/__tests__/claude-session-history.test.js +82 -0
- package/dist/__tests__/claudeTranscriptReader.test.d.ts +1 -0
- package/dist/__tests__/claudeTranscriptReader.test.js +50 -0
- package/dist/__tests__/clone-credential-env.test.d.ts +1 -0
- package/dist/__tests__/clone-credential-env.test.js +81 -0
- package/dist/__tests__/session-git-identity.test.d.ts +1 -0
- package/dist/__tests__/session-git-identity.test.js +58 -0
- package/dist/bridge/agentLaunch.d.ts +44 -0
- package/dist/bridge/agentLaunch.js +7 -0
- package/dist/bridge/checkoutParkAndRemove.d.ts +106 -0
- package/dist/bridge/checkoutParkAndRemove.js +204 -0
- package/dist/bridge/checkoutReclaim.d.ts +11 -0
- package/dist/bridge/checkoutReclaim.js +6 -0
- package/dist/bridge/claudeSessionHistory.d.ts +40 -0
- package/dist/bridge/claudeSessionHistory.js +121 -0
- package/dist/bridge/claudeTranscriptReader.d.ts +21 -0
- package/dist/bridge/claudeTranscriptReader.js +123 -0
- package/dist/bridge/companionForwarder.d.ts +105 -0
- package/dist/bridge/companionForwarder.js +265 -0
- package/dist/bridge/companionFrames.d.ts +22 -0
- package/dist/bridge/companionFrames.js +58 -0
- package/dist/bridge/contextWriter.d.ts +1 -0
- package/dist/bridge/contextWriter.js +13 -2
- package/dist/bridge/diskPreflight.d.ts +95 -0
- package/dist/bridge/diskPreflight.js +124 -0
- package/dist/bridge/listeningPorts.d.ts +36 -0
- package/dist/bridge/listeningPorts.js +66 -0
- package/dist/bridge/localContextWriter.d.ts +1 -0
- package/dist/bridge/localContextWriter.js +11 -2
- package/dist/bridge/relayClient.d.ts +97 -0
- package/dist/bridge/relayClient.js +261 -4
- package/dist/bridge/repoCheckout.d.ts +80 -10
- package/dist/bridge/repoCheckout.js +159 -6
- package/dist/bridge/repoSizeEstimate.d.ts +66 -0
- package/dist/bridge/repoSizeEstimate.js +133 -0
- package/dist/bridge/server.d.ts +29 -0
- package/dist/bridge/server.js +22 -0
- package/dist/bridge/storageQuotaFetch.d.ts +39 -0
- package/dist/bridge/storageQuotaFetch.js +53 -0
- package/dist/bridge/storageQuotaPreflight.d.ts +89 -0
- package/dist/bridge/storageQuotaPreflight.js +85 -0
- package/dist/bridge/workGraphPointer.d.ts +34 -0
- package/dist/bridge/workGraphPointer.js +41 -0
- package/dist/commands/bridge.d.ts +16 -0
- package/dist/commands/bridge.js +279 -10
- package/dist/index.js +0 -0
- package/package.json +1 -1
|
@@ -68,4 +68,79 @@ describe('resolveAgentLaunch (lws-06)', () => {
|
|
|
68
68
|
});
|
|
69
69
|
});
|
|
70
70
|
});
|
|
71
|
+
// psp-01 (production bug, 2026-09-14; supersedes bres-01's `resume`
|
|
72
|
+
// boolean) — the local bridge's per-AGENT session pin. bres-01's
|
|
73
|
+
// `resume: boolean` decided --continue off the CWD alone, so two agents
|
|
74
|
+
// sharing one workspace checkout both resumed whichever conversation was
|
|
75
|
+
// most recently active for that directory — never their own. `sessionPin`
|
|
76
|
+
// pins the launch's session id to the AGENT instead: a prior transcript
|
|
77
|
+
// file for THIS agent → --resume <agentId>; none → --session-id <agentId>
|
|
78
|
+
// (a clean session PINNED to this agent, never a directory-scoped one).
|
|
79
|
+
describe('sessionPin (psp-01)', () => {
|
|
80
|
+
it('sessionPin.exists:false → --session-id <agentId> (first launch, PINS a fresh session)', () => {
|
|
81
|
+
const launch = (0, agentLaunch_1.resolveAgentLaunch)({ sessionPin: { agentId: 'agent-A', exists: false } });
|
|
82
|
+
expect(launch.args).toEqual(['--session-id', 'agent-A']);
|
|
83
|
+
});
|
|
84
|
+
it('sessionPin.exists:true → --resume <agentId> (a prior session for THIS agent already exists)', () => {
|
|
85
|
+
const launch = (0, agentLaunch_1.resolveAgentLaunch)({ sessionPin: { agentId: 'agent-A', exists: true } });
|
|
86
|
+
expect(launch.args).toEqual(['--resume', 'agent-A']);
|
|
87
|
+
});
|
|
88
|
+
it('stamps WENRWA_AGENT_ID into env whenever a sessionPin is given', () => {
|
|
89
|
+
const launch = (0, agentLaunch_1.resolveAgentLaunch)({ sessionPin: { agentId: 'agent-A', exists: true } });
|
|
90
|
+
expect(launch.env).toEqual({ HIVEOP_CONTEXT_FILE: '.hiveop/context.md', WENRWA_AGENT_ID: 'agent-A' });
|
|
91
|
+
});
|
|
92
|
+
it('omitting sessionPin entirely is byte-identical to today\'s bare shape (no args, no WENRWA_AGENT_ID)', () => {
|
|
93
|
+
expect((0, agentLaunch_1.resolveAgentLaunch)({})).toEqual({
|
|
94
|
+
command: 'claude',
|
|
95
|
+
args: [],
|
|
96
|
+
env: { HIVEOP_CONTEXT_FILE: '.hiveop/context.md' },
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
it('composes with autonomous AND activity — the session-pin flag lands LAST', () => {
|
|
100
|
+
const activity = { settingsPath: '/Users/t/.wen-market/claude-hooks-settings.json', sessionId: 'agent-123', activityFile: '/Users/t/.wen-market/activity.jsonl' };
|
|
101
|
+
const launch = (0, agentLaunch_1.resolveAgentLaunch)({ autonomous: true, activity, sessionPin: { agentId: 'agent-A', exists: true } });
|
|
102
|
+
expect(launch.args).toEqual(['--dangerously-skip-permissions', '--settings', activity.settingsPath, '--resume', 'agent-A']);
|
|
103
|
+
});
|
|
104
|
+
// THE PRODUCTION REPRO at this layer: two different agents in the same
|
|
105
|
+
// cwd (a shared workspace checkout) must never resolve to the same argv.
|
|
106
|
+
it('THE PRODUCTION REPRO: two different agents each get their OWN resume target, not a shared one', () => {
|
|
107
|
+
const forA = (0, agentLaunch_1.resolveAgentLaunch)({ sessionPin: { agentId: 'agent-A', exists: true } });
|
|
108
|
+
const forB = (0, agentLaunch_1.resolveAgentLaunch)({ sessionPin: { agentId: 'agent-B', exists: true } });
|
|
109
|
+
expect(forA.args).not.toEqual(forB.args);
|
|
110
|
+
expect(forA.env.WENRWA_AGENT_ID).not.toBe(forB.env.WENRWA_AGENT_ID);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
// local-workspace-id-env — the local lane never set WENRWA_WORKSPACE_ID
|
|
114
|
+
// (unlike the hosted/relay lane, assembleSessionEnv in relayClient.ts),
|
|
115
|
+
// so the workgraph MCP tools' `workspaceId` parameter stayed a REQUIRED
|
|
116
|
+
// field the model had to guess a UUID for instead of an auto-pinned one
|
|
117
|
+
// (mcp-server/src/tools/workgraph-tools.ts's `ws()` helper). The local
|
|
118
|
+
// bridge's `open` frame already carries a per-session `workspaceId`
|
|
119
|
+
// (server.ts) — threading it through here stamps the same env var the
|
|
120
|
+
// hosted lane stamps, closing the gap without inventing a new lookup.
|
|
121
|
+
describe('workspaceId (local-workspace-id-env)', () => {
|
|
122
|
+
it('stamps WENRWA_WORKSPACE_ID into env when workspaceId is given', () => {
|
|
123
|
+
const launch = (0, agentLaunch_1.resolveAgentLaunch)({ workspaceId: 'ws-1' });
|
|
124
|
+
expect(launch.env).toEqual({ HIVEOP_CONTEXT_FILE: '.hiveop/context.md', WENRWA_WORKSPACE_ID: 'ws-1' });
|
|
125
|
+
});
|
|
126
|
+
it('omitting workspaceId is byte-identical to today\'s bare shape (no WENRWA_WORKSPACE_ID)', () => {
|
|
127
|
+
expect((0, agentLaunch_1.resolveAgentLaunch)({}).env).toEqual({ HIVEOP_CONTEXT_FILE: '.hiveop/context.md' });
|
|
128
|
+
});
|
|
129
|
+
it('composes with autonomous, activity, and sessionPin — independent of the other opts', () => {
|
|
130
|
+
const activity = { settingsPath: '/Users/t/.wen-market/claude-hooks-settings.json', sessionId: 'agent-123', activityFile: '/Users/t/.wen-market/activity.jsonl' };
|
|
131
|
+
const launch = (0, agentLaunch_1.resolveAgentLaunch)({
|
|
132
|
+
autonomous: true,
|
|
133
|
+
activity,
|
|
134
|
+
sessionPin: { agentId: 'agent-A', exists: true },
|
|
135
|
+
workspaceId: 'ws-1',
|
|
136
|
+
});
|
|
137
|
+
expect(launch.env).toEqual({
|
|
138
|
+
HIVEOP_CONTEXT_FILE: '.hiveop/context.md',
|
|
139
|
+
WENRWA_SESSION_ID: 'agent-123',
|
|
140
|
+
HIVEOP_ACTIVITY_FILE: activity.activityFile,
|
|
141
|
+
WENRWA_AGENT_ID: 'agent-A',
|
|
142
|
+
WENRWA_WORKSPACE_ID: 'ws-1',
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
});
|
|
71
146
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/**
|
|
4
|
+
* hcs-06 (docs/prd/hosted-container-storage-reclamation.md, ADR-0008) —
|
|
5
|
+
* checkoutParkAndRemove: the deliberate, user-initiated lever for a DIRTY
|
|
6
|
+
* checkout that automatic reclamation (hcs-04/hcs-05) will never touch.
|
|
7
|
+
*
|
|
8
|
+
* FAILS CLOSED BY CONSTRUCTION: commit -> push -> verify-on-remote -> only
|
|
9
|
+
* then delete (by reusing hcs-05's own checkoutReclaim executor for the
|
|
10
|
+
* delete step, never a second `rm` implementation). Every test below proves
|
|
11
|
+
* one of:
|
|
12
|
+
* - a push failure (rejected / unauthenticated / network / unverifiable)
|
|
13
|
+
* retains the checkout and deletes nothing;
|
|
14
|
+
* - a commit failure retains the checkout and deletes nothing;
|
|
15
|
+
* - a genuine success pushes AND removes;
|
|
16
|
+
* - untracked files ride the commit;
|
|
17
|
+
* - nothing-to-park is reported without deleting anything;
|
|
18
|
+
* - a second concurrent park for the same workspace is refused, not raced.
|
|
19
|
+
*
|
|
20
|
+
* No real git, no real filesystem, no real network — `run` and the
|
|
21
|
+
* `reclaimer` are both injected fakes, same DI discipline as
|
|
22
|
+
* checkoutReclaim.ts's own test suite.
|
|
23
|
+
*/
|
|
24
|
+
const checkoutParkAndRemove_1 = require("../bridge/checkoutParkAndRemove");
|
|
25
|
+
const REPOS = '/data/repos';
|
|
26
|
+
const WS = 'ws-1';
|
|
27
|
+
const TARGET = `${REPOS}/${WS}`;
|
|
28
|
+
/** A scriptable git runner: maps an argv-prefix pattern to a canned result,
|
|
29
|
+
* and records every call so a test can assert exactly what was run
|
|
30
|
+
* (e.g. that untracked files are staged via `-A`). */
|
|
31
|
+
function makeGitRunner(script) {
|
|
32
|
+
const calls = [];
|
|
33
|
+
const run = async (cmd, args) => {
|
|
34
|
+
calls.push({ cmd, args });
|
|
35
|
+
const outcome = script(args);
|
|
36
|
+
if (outcome === 'throw')
|
|
37
|
+
throw new Error('simulated network failure: getaddrinfo ENOTFOUND github.com');
|
|
38
|
+
return outcome;
|
|
39
|
+
};
|
|
40
|
+
return { run, calls };
|
|
41
|
+
}
|
|
42
|
+
/** Default happy-path script: dirty working tree, clean push, matching
|
|
43
|
+
* ls-remote verification. Individual tests override specific branches. */
|
|
44
|
+
function defaultScript(headSha = 'a'.repeat(40)) {
|
|
45
|
+
return (args) => {
|
|
46
|
+
if (args.includes('add'))
|
|
47
|
+
return { stdout: '', code: 0 };
|
|
48
|
+
if (args.includes('status'))
|
|
49
|
+
return { stdout: ' M some/file.ts\n?? new/untracked.ts\n', code: 0 };
|
|
50
|
+
if (args.includes('commit'))
|
|
51
|
+
return { stdout: '', code: 0 };
|
|
52
|
+
if (args.includes('rev-list'))
|
|
53
|
+
return { stdout: '0\n', code: 0 };
|
|
54
|
+
if (args.includes('rev-parse'))
|
|
55
|
+
return { stdout: `${headSha}\n`, code: 0 };
|
|
56
|
+
if (args.includes('push'))
|
|
57
|
+
return { stdout: '', code: 0 };
|
|
58
|
+
if (args.includes('ls-remote'))
|
|
59
|
+
return { stdout: `${headSha}\trefs/heads/park/${WS}\n`, code: 0 };
|
|
60
|
+
return { stdout: '', code: 0 };
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function fakeReclaimer(overrides) {
|
|
64
|
+
const calls = [];
|
|
65
|
+
return {
|
|
66
|
+
calls,
|
|
67
|
+
async reclaim(verdicts) {
|
|
68
|
+
calls.push(verdicts);
|
|
69
|
+
if (overrides?.reclaim)
|
|
70
|
+
return overrides.reclaim(verdicts);
|
|
71
|
+
return verdicts.map((v) => ({ workspaceId: v.workspaceId, outcome: 'removed', bytesFreed: 4096 }));
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function baseDeps(script, reclaimer, exists = () => true) {
|
|
76
|
+
const g = makeGitRunner(script);
|
|
77
|
+
return {
|
|
78
|
+
deps: { reposBaseDir: REPOS, exists, run: g.run, reclaimer },
|
|
79
|
+
calls: g.calls,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
describe('checkoutParkAndRemove (hcs-06)', () => {
|
|
83
|
+
it('parkingBranchName is deterministic from the workspace id', () => {
|
|
84
|
+
expect((0, checkoutParkAndRemove_1.parkingBranchName)('ws-1')).toBe((0, checkoutParkAndRemove_1.parkingBranchName)('ws-1'));
|
|
85
|
+
expect((0, checkoutParkAndRemove_1.parkingBranchName)('ws-1')).not.toBe((0, checkoutParkAndRemove_1.parkingBranchName)('ws-2'));
|
|
86
|
+
expect((0, checkoutParkAndRemove_1.parkingBranchName)('ws-1')).toContain('ws-1');
|
|
87
|
+
});
|
|
88
|
+
it('successful park: work is verified on the remote AND the checkout is removed', async () => {
|
|
89
|
+
const reclaimer = fakeReclaimer();
|
|
90
|
+
const { deps, calls } = baseDeps(defaultScript(), reclaimer);
|
|
91
|
+
const parker = (0, checkoutParkAndRemove_1.createCheckoutParker)(deps);
|
|
92
|
+
const phases = [];
|
|
93
|
+
const result = await parker.park(WS, (p) => phases.push(p));
|
|
94
|
+
expect(result).toMatchObject({ workspaceId: WS, outcome: 'removed', bytesFreed: 4096 });
|
|
95
|
+
expect(result.branch).toBe((0, checkoutParkAndRemove_1.parkingBranchName)(WS));
|
|
96
|
+
expect(phases).toEqual(['committing', 'pushing', 'removing']);
|
|
97
|
+
expect(reclaimer.calls).toEqual([[{ workspaceId: WS, verdict: 'RECLAIM', reason: expect.any(String) }]]);
|
|
98
|
+
// Untracked files are included: `git add -A`, not a narrower add.
|
|
99
|
+
const addCall = calls.find((c) => c.args.includes('add'));
|
|
100
|
+
expect(addCall?.args).toEqual(expect.arrayContaining(['-A']));
|
|
101
|
+
});
|
|
102
|
+
it('push rejected (non-fast-forward) -> checkout retained, zero deletion, reason surfaced', async () => {
|
|
103
|
+
const reclaimer = fakeReclaimer();
|
|
104
|
+
const script = (args) => {
|
|
105
|
+
if (args.includes('add'))
|
|
106
|
+
return { stdout: '', code: 0 };
|
|
107
|
+
if (args.includes('status'))
|
|
108
|
+
return { stdout: ' M file.ts\n', code: 0 };
|
|
109
|
+
if (args.includes('commit'))
|
|
110
|
+
return { stdout: '', code: 0 };
|
|
111
|
+
if (args.includes('rev-list'))
|
|
112
|
+
return { stdout: '0\n', code: 0 };
|
|
113
|
+
if (args.includes('rev-parse'))
|
|
114
|
+
return { stdout: `${'a'.repeat(40)}\n`, code: 0 };
|
|
115
|
+
if (args.includes('push')) {
|
|
116
|
+
return { stdout: '! [rejected] HEAD -> park/ws-1 (non-fast-forward)\n', code: 1 };
|
|
117
|
+
}
|
|
118
|
+
return { stdout: '', code: 0 };
|
|
119
|
+
};
|
|
120
|
+
const { deps } = baseDeps(script, reclaimer);
|
|
121
|
+
const parker = (0, checkoutParkAndRemove_1.createCheckoutParker)(deps);
|
|
122
|
+
const result = await parker.park(WS);
|
|
123
|
+
expect(result.outcome).toBe('retained');
|
|
124
|
+
expect(result.phase).toBe('pushing');
|
|
125
|
+
expect(result.reason).toMatch(/non-fast-forward|rejected/i);
|
|
126
|
+
expect(reclaimer.calls).toEqual([]); // removal is NEVER attempted
|
|
127
|
+
});
|
|
128
|
+
it('push unauthenticated -> checkout retained, zero deletion, reason surfaced', async () => {
|
|
129
|
+
const reclaimer = fakeReclaimer();
|
|
130
|
+
const script = (args) => {
|
|
131
|
+
if (args.includes('add'))
|
|
132
|
+
return { stdout: '', code: 0 };
|
|
133
|
+
if (args.includes('status'))
|
|
134
|
+
return { stdout: ' M file.ts\n', code: 0 };
|
|
135
|
+
if (args.includes('commit'))
|
|
136
|
+
return { stdout: '', code: 0 };
|
|
137
|
+
if (args.includes('rev-list'))
|
|
138
|
+
return { stdout: '0\n', code: 0 };
|
|
139
|
+
if (args.includes('rev-parse'))
|
|
140
|
+
return { stdout: `${'a'.repeat(40)}\n`, code: 0 };
|
|
141
|
+
if (args.includes('push')) {
|
|
142
|
+
return { stdout: 'fatal: could not read Username for \'https://github.com\': terminal prompts disabled\n', code: 1 };
|
|
143
|
+
}
|
|
144
|
+
return { stdout: '', code: 0 };
|
|
145
|
+
};
|
|
146
|
+
const { deps } = baseDeps(script, reclaimer);
|
|
147
|
+
const parker = (0, checkoutParkAndRemove_1.createCheckoutParker)(deps);
|
|
148
|
+
const result = await parker.park(WS);
|
|
149
|
+
expect(result.outcome).toBe('retained');
|
|
150
|
+
expect(result.phase).toBe('pushing');
|
|
151
|
+
expect(result.reason).toMatch(/Username|credentials|auth/i);
|
|
152
|
+
expect(reclaimer.calls).toEqual([]);
|
|
153
|
+
});
|
|
154
|
+
it('network failure during push -> checkout retained, zero deletion, reason surfaced', async () => {
|
|
155
|
+
const reclaimer = fakeReclaimer();
|
|
156
|
+
const script = (args) => {
|
|
157
|
+
if (args.includes('add'))
|
|
158
|
+
return { stdout: '', code: 0 };
|
|
159
|
+
if (args.includes('status'))
|
|
160
|
+
return { stdout: ' M file.ts\n', code: 0 };
|
|
161
|
+
if (args.includes('commit'))
|
|
162
|
+
return { stdout: '', code: 0 };
|
|
163
|
+
if (args.includes('rev-list'))
|
|
164
|
+
return { stdout: '0\n', code: 0 };
|
|
165
|
+
if (args.includes('rev-parse'))
|
|
166
|
+
return { stdout: `${'a'.repeat(40)}\n`, code: 0 };
|
|
167
|
+
if (args.includes('push'))
|
|
168
|
+
return 'throw'; // the injected runner rejects — real ECONNREFUSED/ENOTFOUND shape
|
|
169
|
+
return { stdout: '', code: 0 };
|
|
170
|
+
};
|
|
171
|
+
const { deps } = baseDeps(script, reclaimer);
|
|
172
|
+
const parker = (0, checkoutParkAndRemove_1.createCheckoutParker)(deps);
|
|
173
|
+
const result = await parker.park(WS);
|
|
174
|
+
expect(result.outcome).toBe('retained');
|
|
175
|
+
expect(result.phase).toBe('pushing');
|
|
176
|
+
expect(result.reason).toMatch(/network|ENOTFOUND|failure/i);
|
|
177
|
+
expect(reclaimer.calls).toEqual([]);
|
|
178
|
+
});
|
|
179
|
+
it('push reports success but the remote ref does not verify -> retained, zero deletion (never trust exit code alone)', async () => {
|
|
180
|
+
const reclaimer = fakeReclaimer();
|
|
181
|
+
const script = (args) => {
|
|
182
|
+
if (args.includes('add'))
|
|
183
|
+
return { stdout: '', code: 0 };
|
|
184
|
+
if (args.includes('status'))
|
|
185
|
+
return { stdout: ' M file.ts\n', code: 0 };
|
|
186
|
+
if (args.includes('commit'))
|
|
187
|
+
return { stdout: '', code: 0 };
|
|
188
|
+
if (args.includes('rev-list'))
|
|
189
|
+
return { stdout: '0\n', code: 0 };
|
|
190
|
+
if (args.includes('rev-parse'))
|
|
191
|
+
return { stdout: `${'a'.repeat(40)}\n`, code: 0 };
|
|
192
|
+
if (args.includes('push'))
|
|
193
|
+
return { stdout: '', code: 0 }; // claims success...
|
|
194
|
+
if (args.includes('ls-remote'))
|
|
195
|
+
return { stdout: '', code: 0 }; // ...but the ref isn't actually there
|
|
196
|
+
return { stdout: '', code: 0 };
|
|
197
|
+
};
|
|
198
|
+
const { deps } = baseDeps(script, reclaimer);
|
|
199
|
+
const parker = (0, checkoutParkAndRemove_1.createCheckoutParker)(deps);
|
|
200
|
+
const result = await parker.park(WS);
|
|
201
|
+
expect(result.outcome).toBe('retained');
|
|
202
|
+
expect(result.phase).toBe('pushing');
|
|
203
|
+
expect(result.reason).toMatch(/verif|remote/i);
|
|
204
|
+
expect(reclaimer.calls).toEqual([]);
|
|
205
|
+
});
|
|
206
|
+
it('push reports success but ls-remote returns a DIFFERENT sha -> retained, zero deletion', async () => {
|
|
207
|
+
const reclaimer = fakeReclaimer();
|
|
208
|
+
const localSha = 'a'.repeat(40);
|
|
209
|
+
const otherSha = 'b'.repeat(40);
|
|
210
|
+
const script = (args) => {
|
|
211
|
+
if (args.includes('add'))
|
|
212
|
+
return { stdout: '', code: 0 };
|
|
213
|
+
if (args.includes('status'))
|
|
214
|
+
return { stdout: ' M file.ts\n', code: 0 };
|
|
215
|
+
if (args.includes('commit'))
|
|
216
|
+
return { stdout: '', code: 0 };
|
|
217
|
+
if (args.includes('rev-list'))
|
|
218
|
+
return { stdout: '0\n', code: 0 };
|
|
219
|
+
if (args.includes('rev-parse'))
|
|
220
|
+
return { stdout: `${localSha}\n`, code: 0 };
|
|
221
|
+
if (args.includes('push'))
|
|
222
|
+
return { stdout: '', code: 0 };
|
|
223
|
+
if (args.includes('ls-remote'))
|
|
224
|
+
return { stdout: `${otherSha}\trefs/heads/park/${WS}\n`, code: 0 };
|
|
225
|
+
return { stdout: '', code: 0 };
|
|
226
|
+
};
|
|
227
|
+
const { deps } = baseDeps(script, reclaimer);
|
|
228
|
+
const parker = (0, checkoutParkAndRemove_1.createCheckoutParker)(deps);
|
|
229
|
+
const result = await parker.park(WS);
|
|
230
|
+
expect(result.outcome).toBe('retained');
|
|
231
|
+
expect(reclaimer.calls).toEqual([]);
|
|
232
|
+
});
|
|
233
|
+
it('commit fails -> checkout retained, zero deletion, reason surfaced, push never attempted', async () => {
|
|
234
|
+
const reclaimer = fakeReclaimer();
|
|
235
|
+
const script = (args) => {
|
|
236
|
+
if (args.includes('add'))
|
|
237
|
+
return { stdout: '', code: 0 };
|
|
238
|
+
if (args.includes('status'))
|
|
239
|
+
return { stdout: ' M file.ts\n', code: 0 };
|
|
240
|
+
if (args.includes('commit'))
|
|
241
|
+
return { stdout: 'fatal: unable to create index.lock\n', code: 1 };
|
|
242
|
+
return { stdout: '', code: 0 };
|
|
243
|
+
};
|
|
244
|
+
const { deps, calls } = baseDeps(script, reclaimer);
|
|
245
|
+
const parker = (0, checkoutParkAndRemove_1.createCheckoutParker)(deps);
|
|
246
|
+
const result = await parker.park(WS);
|
|
247
|
+
expect(result.outcome).toBe('retained');
|
|
248
|
+
expect(result.phase).toBe('committing');
|
|
249
|
+
expect(result.reason).toMatch(/index\.lock|commit/i);
|
|
250
|
+
expect(reclaimer.calls).toEqual([]);
|
|
251
|
+
expect(calls.some((c) => c.args.includes('push'))).toBe(false);
|
|
252
|
+
});
|
|
253
|
+
it('git add fails -> checkout retained, zero deletion, nothing committed or pushed', async () => {
|
|
254
|
+
const reclaimer = fakeReclaimer();
|
|
255
|
+
const script = (args) => {
|
|
256
|
+
if (args.includes('add'))
|
|
257
|
+
return { stdout: 'fatal: not a git repository\n', code: 128 };
|
|
258
|
+
return { stdout: '', code: 0 };
|
|
259
|
+
};
|
|
260
|
+
const { deps, calls } = baseDeps(script, reclaimer);
|
|
261
|
+
const parker = (0, checkoutParkAndRemove_1.createCheckoutParker)(deps);
|
|
262
|
+
const result = await parker.park(WS);
|
|
263
|
+
expect(result.outcome).toBe('retained');
|
|
264
|
+
expect(result.phase).toBe('committing');
|
|
265
|
+
expect(reclaimer.calls).toEqual([]);
|
|
266
|
+
expect(calls.some((c) => c.args.includes('commit'))).toBe(false);
|
|
267
|
+
expect(calls.some((c) => c.args.includes('push'))).toBe(false);
|
|
268
|
+
});
|
|
269
|
+
it('nothing to commit and nothing unpushed -> nothing-to-park, deletes nothing, never pushes', async () => {
|
|
270
|
+
const reclaimer = fakeReclaimer();
|
|
271
|
+
const script = (args) => {
|
|
272
|
+
if (args.includes('add'))
|
|
273
|
+
return { stdout: '', code: 0 };
|
|
274
|
+
if (args.includes('status'))
|
|
275
|
+
return { stdout: '', code: 0 }; // clean working tree after add
|
|
276
|
+
if (args.includes('rev-list'))
|
|
277
|
+
return { stdout: '0\n', code: 0 }; // nothing unpushed either
|
|
278
|
+
return { stdout: '', code: 0 };
|
|
279
|
+
};
|
|
280
|
+
const { deps, calls } = baseDeps(script, reclaimer);
|
|
281
|
+
const parker = (0, checkoutParkAndRemove_1.createCheckoutParker)(deps);
|
|
282
|
+
const result = await parker.park(WS);
|
|
283
|
+
expect(result.outcome).toBe('nothing-to-park');
|
|
284
|
+
expect(result.branch).toBe((0, checkoutParkAndRemove_1.parkingBranchName)(WS));
|
|
285
|
+
expect(reclaimer.calls).toEqual([]);
|
|
286
|
+
expect(calls.some((c) => c.args.includes('push'))).toBe(false);
|
|
287
|
+
expect(calls.some((c) => c.args.includes('commit'))).toBe(false);
|
|
288
|
+
});
|
|
289
|
+
it('nothing to commit but there ARE unpushed commits -> still pushes and removes', async () => {
|
|
290
|
+
const reclaimer = fakeReclaimer();
|
|
291
|
+
const script = (args) => {
|
|
292
|
+
if (args.includes('add'))
|
|
293
|
+
return { stdout: '', code: 0 };
|
|
294
|
+
if (args.includes('status'))
|
|
295
|
+
return { stdout: '', code: 0 }; // clean working tree
|
|
296
|
+
if (args.includes('rev-list'))
|
|
297
|
+
return { stdout: '2\n', code: 0 }; // but 2 unpushed commits
|
|
298
|
+
if (args.includes('rev-parse'))
|
|
299
|
+
return { stdout: `${'a'.repeat(40)}\n`, code: 0 };
|
|
300
|
+
if (args.includes('push'))
|
|
301
|
+
return { stdout: '', code: 0 };
|
|
302
|
+
if (args.includes('ls-remote'))
|
|
303
|
+
return { stdout: `${'a'.repeat(40)}\trefs/heads/park/${WS}\n`, code: 0 };
|
|
304
|
+
return { stdout: '', code: 0 };
|
|
305
|
+
};
|
|
306
|
+
const { deps, calls } = baseDeps(script, reclaimer);
|
|
307
|
+
const parker = (0, checkoutParkAndRemove_1.createCheckoutParker)(deps);
|
|
308
|
+
const result = await parker.park(WS);
|
|
309
|
+
expect(result.outcome).toBe('removed');
|
|
310
|
+
expect(calls.some((c) => c.args.includes('commit'))).toBe(false); // nothing NEW to commit
|
|
311
|
+
expect(calls.some((c) => c.args.includes('push'))).toBe(true);
|
|
312
|
+
});
|
|
313
|
+
it('push succeeds and verifies, but the reuse-hcs-05 removal step aborts (e.g. a stash reappeared) -> retained, work already safe', async () => {
|
|
314
|
+
const reclaimer = fakeReclaimer({
|
|
315
|
+
reclaim: async (verdicts) => verdicts.map((v) => ({ workspaceId: v.workspaceId, outcome: 'aborted', reason: 'checkout became dirty between the inventory scan and removal — aborted' })),
|
|
316
|
+
});
|
|
317
|
+
const { deps } = baseDeps(defaultScript(), reclaimer);
|
|
318
|
+
const parker = (0, checkoutParkAndRemove_1.createCheckoutParker)(deps);
|
|
319
|
+
const result = await parker.park(WS);
|
|
320
|
+
expect(result.outcome).toBe('retained');
|
|
321
|
+
expect(result.phase).toBe('removing');
|
|
322
|
+
expect(result.reason).toMatch(/dirty|abort/i);
|
|
323
|
+
expect(result.branch).toBe((0, checkoutParkAndRemove_1.parkingBranchName)(WS)); // still findable — the push DID succeed
|
|
324
|
+
});
|
|
325
|
+
it('unsafe workspace id is refused before any git command runs', async () => {
|
|
326
|
+
const reclaimer = fakeReclaimer();
|
|
327
|
+
const { deps, calls } = baseDeps(defaultScript(), reclaimer);
|
|
328
|
+
const parker = (0, checkoutParkAndRemove_1.createCheckoutParker)(deps);
|
|
329
|
+
const result = await parker.park('../escape');
|
|
330
|
+
expect(result.outcome).toBe('refused');
|
|
331
|
+
expect(calls.length).toBe(0);
|
|
332
|
+
expect(reclaimer.calls).toEqual([]);
|
|
333
|
+
});
|
|
334
|
+
it('no checkout exists for this workspace id -> refused, not treated as trivially parked', async () => {
|
|
335
|
+
const reclaimer = fakeReclaimer();
|
|
336
|
+
const { deps } = baseDeps(defaultScript(), reclaimer, () => false);
|
|
337
|
+
const parker = (0, checkoutParkAndRemove_1.createCheckoutParker)(deps);
|
|
338
|
+
const result = await parker.park(WS);
|
|
339
|
+
expect(result.outcome).toBe('refused');
|
|
340
|
+
expect(reclaimer.calls).toEqual([]);
|
|
341
|
+
});
|
|
342
|
+
it('a second concurrent park for the same checkout is refused, not raced', async () => {
|
|
343
|
+
const reclaimer = fakeReclaimer();
|
|
344
|
+
let releasePush;
|
|
345
|
+
const pushGate = new Promise((r) => { releasePush = r; });
|
|
346
|
+
let pushCalls = 0;
|
|
347
|
+
const script = (args) => {
|
|
348
|
+
if (args.includes('add'))
|
|
349
|
+
return { stdout: '', code: 0 };
|
|
350
|
+
if (args.includes('status'))
|
|
351
|
+
return { stdout: ' M file.ts\n', code: 0 };
|
|
352
|
+
if (args.includes('commit'))
|
|
353
|
+
return { stdout: '', code: 0 };
|
|
354
|
+
if (args.includes('rev-list'))
|
|
355
|
+
return { stdout: '0\n', code: 0 };
|
|
356
|
+
if (args.includes('rev-parse'))
|
|
357
|
+
return { stdout: `${'a'.repeat(40)}\n`, code: 0 };
|
|
358
|
+
if (args.includes('push')) {
|
|
359
|
+
pushCalls++;
|
|
360
|
+
return { stdout: '', code: 0 };
|
|
361
|
+
}
|
|
362
|
+
if (args.includes('ls-remote'))
|
|
363
|
+
return { stdout: `${'a'.repeat(40)}\trefs/heads/park/${WS}\n`, code: 0 };
|
|
364
|
+
return { stdout: '', code: 0 };
|
|
365
|
+
};
|
|
366
|
+
// Wrap the runner so the FIRST push call blocks on pushGate, giving the
|
|
367
|
+
// second park() call a real window to race in.
|
|
368
|
+
const g = makeGitRunner(script);
|
|
369
|
+
const gatedRun = async (cmd, args) => {
|
|
370
|
+
if (args.includes('push'))
|
|
371
|
+
await pushGate;
|
|
372
|
+
return g.run(cmd, args);
|
|
373
|
+
};
|
|
374
|
+
const parker = (0, checkoutParkAndRemove_1.createCheckoutParker)({ reposBaseDir: REPOS, exists: () => true, run: gatedRun, reclaimer });
|
|
375
|
+
const first = parker.park(WS);
|
|
376
|
+
// Let the first call reach (and block inside) the push phase.
|
|
377
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
378
|
+
const second = await parker.park(WS);
|
|
379
|
+
expect(second.outcome).toBe('refused');
|
|
380
|
+
expect(second.reason).toMatch(/already.*progress|in progress/i);
|
|
381
|
+
releasePush();
|
|
382
|
+
const firstResult = await first;
|
|
383
|
+
expect(firstResult.outcome).toBe('removed');
|
|
384
|
+
expect(pushCalls).toBe(1); // never raced into a second push
|
|
385
|
+
});
|
|
386
|
+
});
|
|
@@ -329,7 +329,9 @@ describe('checkoutReclaim + repoCheckout — re-clone after reclaim', () => {
|
|
|
329
329
|
// 1. Before reclaim: the terminal reuses the existing checkout.
|
|
330
330
|
const before = await (0, repoCheckout_1.ensureWorkspaceRepo)({ workspaceId, repoUrl }, repoDeps);
|
|
331
331
|
expect(before).toEqual({ path: dirPath, action: 'existing' });
|
|
332
|
-
|
|
332
|
+
// grad-04: reuse now also verifies `origin` matches the workspace's repo,
|
|
333
|
+
// so count CLONES — the invariant this test is named for — not every git call.
|
|
334
|
+
expect(cloneRuns.filter((r) => r.includes('clone'))).toHaveLength(0);
|
|
333
335
|
// 2. Reclaim removes the checkout directory entirely.
|
|
334
336
|
const reclaimDeps = {
|
|
335
337
|
reposBaseDir: REPOS,
|
|
@@ -348,6 +350,6 @@ describe('checkoutReclaim + repoCheckout — re-clone after reclaim', () => {
|
|
|
348
350
|
// the repo (action: 'cloned'), never 'failed' (the home-dir degrade).
|
|
349
351
|
const after = await (0, repoCheckout_1.ensureWorkspaceRepo)({ workspaceId, repoUrl }, repoDeps);
|
|
350
352
|
expect(after).toEqual({ path: dirPath, action: 'cloned' });
|
|
351
|
-
expect(cloneRuns).toHaveLength(1);
|
|
353
|
+
expect(cloneRuns.filter((r) => r.includes('clone'))).toHaveLength(1);
|
|
352
354
|
});
|
|
353
355
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/**
|
|
4
|
+
* bws-08 — pure resolver for the companion forwarder's env-derived knobs
|
|
5
|
+
* (buildCompanionForwarderEnvOptions in commands/bridge.ts). Same
|
|
6
|
+
* "pure helper, thin command shell" pattern as buildBrowserDriverOptions
|
|
7
|
+
* (bridge-command-browser-options.test.ts) — `buildCompanionForwarder`
|
|
8
|
+
* itself is thin glue over real `/etc/hosts` + `/proc/net/tcp[6]` reads and
|
|
9
|
+
* is deliberately left untested here for the same reason the commander
|
|
10
|
+
* action itself is: the actual decisions (resolveFlyLocal6pnHost,
|
|
11
|
+
* createCompanionForwarder, createNoopCompanionForwarder) are each unit
|
|
12
|
+
* tested directly in bridge-companion-forwarder.test.ts.
|
|
13
|
+
*/
|
|
14
|
+
const bridge_1 = require("../commands/bridge");
|
|
15
|
+
describe('buildCompanionForwarderEnvOptions (bws-08)', () => {
|
|
16
|
+
it('defaults to a 5s poll and no excluded ports', () => {
|
|
17
|
+
expect((0, bridge_1.buildCompanionForwarderEnvOptions)({})).toEqual({
|
|
18
|
+
pollMs: 5_000,
|
|
19
|
+
excludePorts: new Set(),
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
it('honors HT_COMPANION_FORWARDER_POLL_MS', () => {
|
|
23
|
+
expect((0, bridge_1.buildCompanionForwarderEnvOptions)({ HT_COMPANION_FORWARDER_POLL_MS: '2000' })).toEqual({
|
|
24
|
+
pollMs: 2000,
|
|
25
|
+
excludePorts: new Set(),
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
it('parses a comma-separated HT_COMPANION_FORWARDER_EXCLUDE_PORTS list', () => {
|
|
29
|
+
expect((0, bridge_1.buildCompanionForwarderEnvOptions)({ HT_COMPANION_FORWARDER_EXCLUDE_PORTS: '9229, 8377,3000' })).toEqual({
|
|
30
|
+
pollMs: 5_000,
|
|
31
|
+
excludePorts: new Set([9229, 8377, 3000]),
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
it('ignores garbage entries in the exclude-ports list rather than crashing', () => {
|
|
35
|
+
expect((0, bridge_1.buildCompanionForwarderEnvOptions)({ HT_COMPANION_FORWARDER_EXCLUDE_PORTS: 'nope,,-1,0,8080' })).toEqual({
|
|
36
|
+
pollMs: 5_000,
|
|
37
|
+
excludePorts: new Set([8080]),
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
it('falls back to the default poll on a non-numeric override', () => {
|
|
41
|
+
expect((0, bridge_1.buildCompanionForwarderEnvOptions)({ HT_COMPANION_FORWARDER_POLL_MS: 'abc' })).toEqual({
|
|
42
|
+
pollMs: 5_000,
|
|
43
|
+
excludePorts: new Set(),
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|