@ran-sh/dsh-crew 2.2.0 → 2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/package.json +1 -1
  3. package/scripts/setup.mjs +6 -27
  4. package/scripts/verify-npm-install.mjs +2 -2
  5. package/src/adaptive-routing.mjs +2 -2
  6. package/src/ci-evidence.mjs +1 -1
  7. package/src/client/quick-panel.tsx +0 -1
  8. package/src/credential-lifecycle.mjs +0 -3
  9. package/src/credential-purge-state.mjs +1 -5
  10. package/src/credential-reference.mjs +0 -8
  11. package/src/crew-workspaces.mjs +1 -1
  12. package/src/delivery.mjs +2 -2
  13. package/src/dsh-cli-runtime.mjs +18 -22
  14. package/src/extension-contract.mjs +1 -1
  15. package/src/failure-classification.mjs +0 -10
  16. package/src/hub/entry.mjs +1 -1
  17. package/src/hub/index.mjs +0 -1
  18. package/src/i18n.mjs +0 -2
  19. package/src/install/crew-skill.mjs +1 -1
  20. package/src/install/install-legacy.mjs +3 -3
  21. package/src/install/npx-lifecycle.mjs +60 -112
  22. package/src/install/official-web.mjs +1 -1
  23. package/src/install/windows-startup.mjs +5 -5
  24. package/src/install/windows-supervisor-adapter.mjs +3 -2
  25. package/src/install/windows-supervisor-lifecycle.mjs +2 -2
  26. package/src/install/zcode.mjs +0 -2
  27. package/src/job-contracts.mjs +1 -1
  28. package/src/job-identity.mjs +2 -2
  29. package/src/jobs.mjs +3 -4
  30. package/src/model-routing.mjs +3 -4
  31. package/src/model-schedule.mjs +0 -1
  32. package/src/policy-legacy.mjs +10 -12
  33. package/src/provider-inventory.mjs +1 -1
  34. package/src/provider-layer-migration.mjs +0 -2
  35. package/src/readiness-matrix.mjs +1 -1
  36. package/src/release-in-use.mjs +1 -1
  37. package/src/role-profiles.mjs +2 -2
  38. package/src/runtime-controls.mjs +1 -1
  39. package/src/runtime-identity.mjs +1 -1
  40. package/src/status-shard.mjs +1 -1
  41. package/src/supervisor/restart-request.mjs +4 -4
  42. package/src/workflow-runtime.mjs +1 -2
  43. package/src/workspace-context.mjs +2 -2
  44. package/src/workspace-isolation.mjs +4 -4
  45. package/src/workspace-lock.mjs +2 -3
  46. package/scripts/policy-probe.mjs +0 -101
  47. package/scripts/verify-crew-ui-polish.mjs +0 -145
  48. package/scripts/verify-history-ui.mjs +0 -98
  49. package/scripts/verify-installer-fix.mjs +0 -26
@@ -18,18 +18,18 @@ import { randomUUID } from 'node:crypto';
18
18
  import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from 'node:fs';
19
19
  import { dirname, join } from 'node:path';
20
20
 
21
- export const RESTART_REQUEST_SCHEMA = 1;
22
- export const RESTART_REQUEST_TTL_MS = 60_000;
21
+ const RESTART_REQUEST_SCHEMA = 1;
22
+ const RESTART_REQUEST_TTL_MS = 60_000;
23
23
 
24
24
  export function supervisorStateRoot(appRoot) {
25
25
  return join(appRoot, 'supervisor');
26
26
  }
27
27
 
28
- export function restartRequestsDir(appRoot) {
28
+ function restartRequestsDir(appRoot) {
29
29
  return join(supervisorStateRoot(appRoot), 'restart-requests');
30
30
  }
31
31
 
32
- export function restartResultsDir(appRoot) {
32
+ function restartResultsDir(appRoot) {
33
33
  return join(supervisorStateRoot(appRoot), 'restart-results');
34
34
  }
35
35
 
@@ -31,7 +31,6 @@ export const WORKFLOW_ERROR_CODES = {
31
31
  REVIEWER_MUTATED_CANDIDATE: 'REVIEWER_MUTATED_CANDIDATE',
32
32
  };
33
33
 
34
- const TERMINAL = new Set([JOB_PHASES.COMPLETED, JOB_PHASES.FAILED, JOB_PHASES.CANCELLED, JOB_PHASES.INTERRUPTED]);
35
34
 
36
35
  function lines(value) {
37
36
  if (typeof value !== 'string' || value.trim() === '') return [];
@@ -817,4 +816,4 @@ export function createWorkflowRuntime(adapters, {
817
816
  return { start, wait, get, list, cancel, runtimeState, setMaxParallel, refreshRuntimeControls };
818
817
  }
819
818
 
820
- export { JOB_PHASES, TERMINAL };
819
+ export { JOB_PHASES };
@@ -6,7 +6,7 @@ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync
6
6
  import { homedir } from 'node:os';
7
7
  import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
8
8
 
9
- export const WORKSPACE_CONTEXT_SCHEMA_VERSION = 1;
9
+ const WORKSPACE_CONTEXT_SCHEMA_VERSION = 1;
10
10
  const ID = /^[a-z0-9][a-z0-9._-]{0,63}$/;
11
11
 
12
12
  export function isSafeBranchName(value) {
@@ -51,7 +51,7 @@ function normalizeContext(id, raw) {
51
51
  };
52
52
  }
53
53
 
54
- export function workspaceContextsFile({ home = homedir() } = {}) {
54
+ function workspaceContextsFile({ home = homedir() } = {}) {
55
55
  return join(home, '.config', 'dsh-crew', 'workspaces.json');
56
56
  }
57
57
 
@@ -28,9 +28,9 @@ export const GIT_ERROR = 'GIT_ERROR';
28
28
  /** A valid repository whose HEAD does not resolve because nothing is committed. */
29
29
  export const REPOSITORY_HAS_NO_COMMITS = 'REPOSITORY_HAS_NO_COMMITS';
30
30
  export const WORKTREE_LOCKED = 'WORKTREE_LOCKED';
31
- export const WORKTREE_RESERVE_FAILED = 'WORKTREE_RESERVE_FAILED';
31
+ const WORKTREE_RESERVE_FAILED = 'WORKTREE_RESERVE_FAILED';
32
32
  export const CANDIDATE_CAPTURE_FAILED = 'CANDIDATE_CAPTURE_FAILED';
33
- export const MAX_PARALLEL_CAP = 16;
33
+ const MAX_PARALLEL_CAP = 16;
34
34
  export const DEFAULT_MAX_PARALLEL = 3;
35
35
  // Worktree names read `Crew_YYYYMMDD_HHMMSS_<purpose>` so an operator can tell
36
36
  // from the directory alone when a job ran and what it was for. Worktrees from
@@ -642,8 +642,8 @@ async function ownershipDrift(run, worktreePath, expect) {
642
642
  * the registration is gone and no directory remains — success is never claimed
643
643
  * while a worktree stays registered or on disk.
644
644
  */
645
- export const WORKTREE_CLEANUP_RETRIES = 3;
646
- export const WORKTREE_CLEANUP_BACKOFF_MS = 150;
645
+ const WORKTREE_CLEANUP_RETRIES = 3;
646
+ const WORKTREE_CLEANUP_BACKOFF_MS = 150;
647
647
 
648
648
  /**
649
649
  * Is `worktreePath` a worktree Crew created, for this repository?
@@ -23,11 +23,10 @@ import { dirname } from 'node:path';
23
23
  import { randomBytes } from 'node:crypto';
24
24
 
25
25
  export const WORKSPACE_BUSY = 'WORKSPACE_BUSY';
26
- export const WORKSPACE_LOCK_FILE = 'workspace.lock';
27
- export const WORKSPACE_LOCK_SCHEMA = 1;
26
+ const WORKSPACE_LOCK_SCHEMA = 1;
28
27
 
29
28
  export const DEFAULT_LOCK_WAIT_MS = 120_000;
30
- export const DEFAULT_LOCK_POLL_MS = 500;
29
+ const DEFAULT_LOCK_POLL_MS = 500;
31
30
  // The two reasons a lock outlives its job are not the same, and they are covered
32
31
  // by different things. A holder whose process died is caught immediately by the
33
32
  // pid check below. This bound covers the other case — the process is alive but
@@ -1,101 +0,0 @@
1
- // Functional probe of the real MCP stdio server (src/server.mjs), no DSH
2
- // instance and no DeepSeek credentials needed: policy rejections must happen
3
- // before any worker runtime is touched.
4
- // Run: node scripts/policy-probe.mjs
5
-
6
- import { spawn } from 'node:child_process';
7
-
8
- const child = spawn(process.execPath, ['src/server.mjs'], { stdio: ['pipe', 'pipe', 'pipe'] });
9
- let buffer = '';
10
- const pending = new Map();
11
- let nextId = 1;
12
-
13
- child.stdout.on('data', (d) => {
14
- buffer += d.toString();
15
- let idx;
16
- while ((idx = buffer.indexOf('\n')) >= 0) {
17
- const line = buffer.slice(0, idx);
18
- buffer = buffer.slice(idx + 1);
19
- if (!line.trim()) continue;
20
- let msg;
21
- try { msg = JSON.parse(line); } catch { continue; }
22
- if (msg.id !== undefined && pending.has(msg.id)) {
23
- pending.get(msg.id)(msg);
24
- pending.delete(msg.id);
25
- }
26
- }
27
- });
28
-
29
- function call(method, params) {
30
- return new Promise((resolve) => {
31
- const id = nextId++;
32
- pending.set(id, resolve);
33
- child.stdin.write(JSON.stringify({ jsonrpc: '2.0', id, method, params }) + '\n');
34
- });
35
- }
36
-
37
- const results = [];
38
- function check(name, cond, detail) {
39
- results.push({ name, ok: !!cond, detail });
40
- console.log(`${cond ? 'PASS' : 'FAIL'} ${name}${detail ? ` — ${detail}` : ''}`);
41
- }
42
-
43
- await call('initialize', { protocolVersion: '2025-03-26', capabilities: {}, clientInfo: { name: 'probe', version: '1' } });
44
- await new Promise((r) => setTimeout(r, 200));
45
-
46
- // 1. Read config: must expose the new effective policy fields.
47
- const cfg = await call('tools/call', { name: 'dsh_worker_config', arguments: {} });
48
- const cfgText = cfg.result?.content?.[0]?.text;
49
- let cfgJson = null;
50
- try { cfgJson = JSON.parse(cfgText); } catch {}
51
- check('config returns effective policy', !!cfgJson && typeof cfgJson.flash_state === 'string' && typeof cfgJson.pro_state === 'string' && typeof cfgJson.routing_guidance === 'string',
52
- cfgJson ? `flash=${cfgJson.flash_state} pro=${cfgJson.pro_state} mode=${cfgJson.collaboration_mode}` : cfgText?.slice(0, 120));
53
- check('config hides API keys', cfgText !== undefined && !/api_key|sk-/.test(cfgText));
54
- check('config reports effective_policy + roles + legacy_source', !!cfgJson
55
- && typeof cfgJson.effective_policy === 'string' && cfgJson.effective_policy.includes('mode=')
56
- && Array.isArray(cfgJson.flash_roles) && cfgJson.flash_roles.length > 0
57
- && Array.isArray(cfgJson.pro_roles) && cfgJson.pro_roles.length > 0
58
- && typeof cfgJson.legacy_source === 'string',
59
- cfgJson ? cfgJson.effective_policy : undefined);
60
- check('config reflects main_agent_mode', !!cfgJson && ['direct-allowed', 'coordinator-first', 'dispatcher-only'].includes(cfgJson.main_agent_mode), cfgJson?.main_agent_mode);
61
-
62
- // 2. Session clamp: flash-only + explicit pro must be refused with TIER_DISABLED.
63
- const clamp = await call('tools/call', { name: 'dsh_worker_config', arguments: { tier_policy: 'flash-only' } });
64
- check('session tier_policy clamp set', clamp.result !== undefined && !clamp.result.isError);
65
-
66
- const refusePro = await call('tools/call', { name: 'dsh_run_worker', arguments: { task: 'probe', tier: 'pro' } });
67
- const refuseText = refusePro.result?.content?.[0]?.text ?? '';
68
- check('pro refused under flash-only', /TIER_DISABLED/.test(refuseText), refuseText.slice(0, 120));
69
-
70
- // 3. subagents master switch via session enabled=false.
71
- const off = await call('tools/call', { name: 'dsh_worker_config', arguments: { reset: true, enabled: false } });
72
- check('session enabled=false set', off.result !== undefined && !off.result.isError);
73
- const refuseAll = await call('tools/call', { name: 'dsh_run_worker', arguments: { task: 'probe' } });
74
- const refuseAllText = refuseAll.result?.content?.[0]?.text ?? '';
75
- check('dispatch refused when disabled', /SUBAGENTS_DISABLED/.test(refuseAllText), refuseAllText.slice(0, 120));
76
-
77
- // 4. dsh_spawn_worker shares the same policy.
78
- const refuseSpawn = await call('tools/call', { name: 'dsh_spawn_worker', arguments: { task: 'probe' } });
79
- const refuseSpawnText = refuseSpawn.result?.content?.[0]?.text ?? '';
80
- check('spawn refused when disabled', /SUBAGENTS_DISABLED/.test(refuseSpawnText));
81
-
82
- // 5. Manual semantics: flash manual + pro auto, no explicit tier → pro is chosen, never flash.
83
- const manual = await call('tools/call', { name: 'dsh_worker_config', arguments: { reset: true, tier_policy: 'auto' } });
84
- // global config is balanced here; exercise manual via session state override:
85
- const manualSet = await call('tools/call', { name: 'dsh_worker_config', arguments: { collaboration_mode: 'custom', flash_state: 'manual', pro_state: 'auto', default_tier: 'flash' } });
86
- check('manual/custom session set', manualSet.result !== undefined && !manualSet.result.isError);
87
- const manualCfg = await call('tools/call', { name: 'dsh_worker_config', arguments: {} });
88
- const manualCfgJson = JSON.parse(manualCfg.result.content[0].text);
89
- check('manual tier never auto-default (effective_default_tier=pro)',
90
- manualCfgJson.effective_default_tier === 'pro', `got ${manualCfgJson.effective_default_tier}`);
91
- check('effective states reported', manualCfgJson.flash_state === 'manual' && manualCfgJson.pro_state === 'auto');
92
-
93
- // 6. Reset back to global defaults.
94
- const reset = await call('tools/call', { name: 'dsh_worker_config', arguments: { reset: true } });
95
- check('reset restores defaults', reset.result !== undefined && !reset.result.isError);
96
-
97
- child.kill();
98
- await new Promise((r) => setTimeout(r, 100));
99
- const failed = results.filter((r) => !r.ok);
100
- console.log(`\n${results.length - failed.length}/${results.length} checks passed`);
101
- process.exit(failed.length > 0 ? 1 : 0);
@@ -1,145 +0,0 @@
1
- // Isolated browser acceptance of the shipped modules. All HTTP requests are
2
- // intercepted: no real Crew configuration or host browser profile is accessed.
3
- import assert from 'node:assert/strict';
4
- import { createRequire } from 'node:module';
5
- import { readFileSync, mkdtempSync } from 'node:fs';
6
- import { tmpdir } from 'node:os';
7
- import { join, dirname } from 'node:path';
8
- import { fileURLToPath } from 'node:url';
9
-
10
- const require = createRequire(import.meta.url);
11
- const { chromium } = require(process.env.CREW_PLAYWRIGHT_MODULE || 'playwright');
12
- const root = dirname(dirname(fileURLToPath(import.meta.url)));
13
- const output = mkdtempSync(join(tmpdir(), 'crew-ui-polish-'));
14
- const browser = await chromium.launch({ headless: true,
15
- ...(process.env.CREW_BROWSER_EXE ? { executablePath: process.env.CREW_BROWSER_EXE } : {}),
16
- });
17
- const errors = [];
18
- const results = [];
19
- const baseConfig = {
20
- subagents_enabled: true, collaboration_mode: 'balanced', main_agent_mode: 'direct-allowed',
21
- flash_state: 'auto', pro_state: 'auto', isolation: 'worktree', max_parallel: 16,
22
- default_timeout_seconds: 1800,
23
- default_tier: 'flash', default_effort: 'max', mode: 'auto',
24
- vision_enabled: true, imagegen_enabled: false, vision_provider: 'example', imagegen_provider: 'example',
25
- flash_model_priority: [
26
- { provider: 'example-provider', model: 'long-model-name-for-responsive-layout-check' },
27
- { provider: 'example-provider', model: 'second-model' },
28
- ], pro_model_priority: [],
29
- };
30
-
31
- async function mount(surface, locale = 'zh') {
32
- const page = await browser.newPage({ viewport: { width: 1000, height: 1000 } });
33
- page.on('pageerror', error => errors.push(error.message));
34
- let config = structuredClone(baseConfig);
35
- const patches = [];
36
- await page.route('**/*', async route => {
37
- const request = route.request();
38
- const url = new URL(request.url());
39
- if (url.origin !== 'http://127.0.0.1:41790') return route.abort();
40
- if (url.pathname === '/') return route.fulfill({ contentType: 'text/html', body:
41
- '<!doctype html><html><head><meta charset="utf-8"><style>body{margin:0;background:#f6f7f9;color:#20252b;font-family:Arial,sans-serif}main{max-width:760px;margin:24px auto;padding:20px;background:white;border-radius:16px}@media(prefers-color-scheme:dark){body{background:#16191d;color:#e5e7eb}main{background:#20242a}}</style></head><body><main id="root"></main></body></html>' });
42
- const suffix = url.pathname.replace('/_dsh/dsh-crew', '');
43
- let body = { ok: true };
44
- if (suffix === '/bridge-status') body = surface === 'quick' ? { ok: true, surface: 'official-bridge' } : { ok: false };
45
- else if (suffix === '/runtime') body = { ok: true, service: 'dsh-crew-hub', profile: 'dsh-crew', listen_port: 3210, execution_plane: 'hub-3210', runtime_version: 'test', protocol_version: 1 };
46
- else if (suffix === '/quick-config' && request.method() === 'POST') {
47
- const patch = request.postDataJSON(); patches.push(patch); config = { ...config, ...patch }; body = { ok: true, config };
48
- } else if (suffix === '/quick-status' || suffix === '/config') body = { ok: true, config };
49
- else if (suffix === '/jobs') body = { ok: true, jobs: [] };
50
- else if (suffix === '/install/status') body = { ok: true, status: { codex: { ready: true, installed: true }, claude: { ready: true, installed: true }, zcode: { ready: true, installed: true } } };
51
- else if (suffix === '/model-catalog' || suffix === '/providers') body = { ok: true, providers: [], records: [] };
52
- return route.fulfill({ contentType: 'application/json', body: JSON.stringify(body) });
53
- });
54
- await page.goto('http://127.0.0.1:41790/');
55
- await page.waitForLoadState('networkidle');
56
- await page.addScriptTag({ content: readFileSync(join(root, 'node_modules/react/umd/react.development.js'), 'utf8') });
57
- await page.addScriptTag({ content: readFileSync(join(root, 'node_modules/react-dom/umd/react-dom.development.js'), 'utf8') });
58
- await page.evaluate(locale => {
59
- const ctx = {
60
- on: () => () => {}, locale: { getLocale: () => ({ active: locale }) },
61
- slots: { inject: (_name, callback) => callback(), register: (_meta, render) => { window.crewRoot.render(render()); return () => {}; } },
62
- };
63
- window.crewRoot = window.ReactDOM.createRoot(document.getElementById('root'));
64
- window.__ModuleLoader__ = { load({ factory }) {
65
- const jsx = (tag, props, key) => window.React.createElement(tag, { ...props, ...(key === undefined ? {} : { key }) });
66
- factory(id => {
67
- if (id === 'react') return window.React;
68
- if (id === 'react-dom' || id === 'react-dom/client') return window.ReactDOM;
69
- if (id === 'react/jsx-runtime') return { jsx, jsxs: jsx, Fragment: window.React.Fragment };
70
- throw Error('Unexpected fixture dependency: ' + id);
71
- }).apply(ctx);
72
- } };
73
- }, locale);
74
- await page.addScriptTag({ content: readFileSync(join(root, surface === 'quick' ? 'official-web-bridge/lib/client.js' : 'lib/client.js'), 'utf8') });
75
- try { await page.locator('.crew-panel-header').waitFor({ timeout: 10000 }); }
76
- catch (error) {
77
- console.error(JSON.stringify({ surface, errors, body: (await page.locator('body').innerText()).slice(0, 1600) }));
78
- throw error;
79
- }
80
- return { page, patches };
81
- }
82
-
83
- try {
84
- const { page, patches } = await mount('quick');
85
- await page.getByRole('link', { name: '打开 3210 后台 →' }).waitFor();
86
- assert.equal(await page.getByRole('link', { name: '打开 3210 后台 →' }).getAttribute('href'), 'http://127.0.0.1:3210/');
87
- await page.screenshot({ path: join(output, '3080-quick.png'), fullPage: true });
88
- const worker = page.locator('details.crew-quick-group').filter({ has: page.locator('summary strong', { hasText: 'Worker / Flash' }) });
89
- await worker.getByRole('button', { name: '下移 long-model-name-for-responsive-layout-check' }).click();
90
- await page.waitForFunction(() => document.querySelector('.crew-quick-list li .crew-quick-model-name')?.textContent === 'second-model');
91
- assert.equal(patches.at(-1).flash_model_priority[0].model, 'second-model');
92
- await worker.getByRole('button', { name: '从优先级移除 second-model' }).click();
93
- await worker.locator('li').first().waitFor();
94
- await worker.locator('.crew-quick-add > summary').click();
95
- await worker.getByRole('textbox', { name: 'Worker / Flash Provider', exact: true }).fill('added-provider');
96
- await worker.getByRole('textbox', { name: 'Worker / Flash 模型', exact: true }).fill('added-model');
97
- await worker.getByRole('button', { name: '+ 添加模型', exact: true }).click();
98
- await worker.getByText('added-model', { exact: true }).waitFor();
99
- assert.equal(patches.at(-1).flash_model_priority.at(-1).model, 'added-model');
100
- const reviewer = page.locator('details.crew-quick-group').filter({ has: page.locator('summary strong', { hasText: 'Reviewer / Pro' }) });
101
- await reviewer.locator(':scope > summary').click();
102
- await reviewer.locator('.crew-quick-add > summary').click();
103
- await reviewer.getByRole('textbox', { name: 'Reviewer / Pro Provider', exact: true }).fill('review-provider');
104
- await reviewer.getByRole('textbox', { name: 'Reviewer / Pro 模型', exact: true }).fill('review-model');
105
- await reviewer.getByRole('button', { name: '+ 添加模型', exact: true }).click();
106
- await reviewer.getByText('review-model', { exact: true }).waitFor();
107
- await page.waitForFunction(() => !document.querySelector('input[type=checkbox]').disabled);
108
- assert.notEqual(await reviewer.getAttribute('open'), null, 'Pro stays expanded after save rerenders');
109
- const multimodal = page.locator('details.crew-quick-group').filter({ has: page.locator('summary strong', { hasText: '多模态' }) });
110
- await multimodal.locator(':scope > summary').click();
111
- await page.getByRole('checkbox', { name: '启用子 Agent' }).uncheck();
112
- await page.waitForFunction(() => !document.querySelector('input[type=checkbox]').disabled);
113
- assert.equal(patches.at(-1).subagents_enabled, false);
114
- assert.notEqual(await reviewer.getAttribute('open'), null, 'Pro stays expanded after other settings change');
115
- assert.notEqual(await multimodal.getAttribute('open'), null, 'Multimodal stays expanded after other settings change');
116
- await worker.locator(':scope > summary').click();
117
- assert.equal(await worker.getAttribute('open'), null);
118
- await page.setViewportSize({ width: 420, height: 900 });
119
- await worker.locator(':scope > summary').click();
120
- assert.equal(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth), true);
121
- await page.screenshot({ path: join(output, '3080-narrow.png'), fullPage: true });
122
- await page.emulateMedia({ colorScheme: 'dark' });
123
- await page.screenshot({ path: join(output, '3080-dark.png'), fullPage: true });
124
- results.push('quick: reorder/remove/add/toggle/collapse; Pro and multimodal stay open across save rerenders; narrow/dark');
125
- await page.close();
126
-
127
- const { page: backend } = await mount('full');
128
- const backLink = backend.getByRole('link', { name: '打开 3080 官方界面 →' });
129
- assert.equal(await backLink.getAttribute('href'), 'http://127.0.0.1:3080/');
130
- await backend.getByRole('button', { name: '全部折叠', exact: true }).click();
131
- await backend.screenshot({ path: join(output, '3210-backend.png'), fullPage: true });
132
- await backend.getByRole('button', { name: '全部展开', exact: true }).click();
133
- assert.equal(await backend.locator('.crew-section-trigger[aria-expanded=false]').count(), 0);
134
- await backend.getByRole('button', { name: '全部折叠', exact: true }).click();
135
- await backend.setViewportSize({ width: 420, height: 900 });
136
- assert.equal(await backend.evaluate(() => document.documentElement.scrollWidth <= innerWidth), true);
137
- await backend.screenshot({ path: join(output, '3210-narrow.png'), fullPage: true });
138
- results.push('backend: reciprocal link/expand all/collapse all/narrow');
139
- await backend.close();
140
- const { page: english } = await mount('quick', 'en');
141
- await english.getByRole('button', { name: 'Move down long-model-name-for-responsive-layout-check' }).waitFor();
142
- await english.close();
143
- assert.deepEqual(errors, []);
144
- console.log(JSON.stringify({ ok: true, results, consoleErrors: errors, screenshots: output }));
145
- } finally { await browser.close(); }
@@ -1,98 +0,0 @@
1
- // Full browser -> HTTP handler -> controller -> executor -> real TEMP files.
2
- // Supervisor process control is simulated; never addresses real 3080/3210.
3
- import assert from 'node:assert/strict';
4
- import { createRequire } from 'node:module';
5
- import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, existsSync, rmSync } from 'node:fs';
6
- import { join, dirname } from 'node:path';
7
- import { tmpdir } from 'node:os';
8
- import { fileURLToPath } from 'node:url';
9
- import { Readable } from 'node:stream';
10
- import { createHistoryService } from '../src/history/service.mjs';
11
- import { registerHistoryHttp } from '../src/history/http.mjs';
12
- import { runHistoryOperation } from '../src/history/operation.mjs';
13
- import { CREW_SESSIONS_REL } from '../src/install/crew-paths.mjs';
14
-
15
- const require = createRequire(import.meta.url);
16
- const { chromium } = require(process.env.CREW_PLAYWRIGHT_MODULE || 'playwright');
17
- const repo = dirname(dirname(fileURLToPath(import.meta.url)));
18
- const root = mkdtempSync(join(tmpdir(), 'crew-history-e2e-'));
19
- const output = mkdtempSync(join(tmpdir(), 'crew-history-ui-shots-'));
20
- mkdirSync(join(root, 'harness/storages'), { recursive: true });
21
- mkdirSync(join(root, CREW_SESSIONS_REL, 'example/session-test'), { recursive: true });
22
- const file = join(root, CREW_SESSIONS_REL, 'example/session-test/session.jsonl');
23
- writeFileSync(file, 'DISPOSABLE TEST CONVERSATION');
24
- writeFileSync(join(root, 'harness/storages/workspace.json'), JSON.stringify({ unit: { name: 'workspace', version: 2 }, global: { initialized: true, workspaceIds: ['test-workspace'], archivedSessionIds: [] }, tables: { workspaces: { 'test-workspace': { path: '/example/project', title: 'Disposable workspace', createdAt: '2026-01-01T00:00:00Z', updatedAt: '2026-01-01T00:00:00Z', sessionIds: ['session-test'] } } } }));
25
- const errors = []; let stopped = false; let operationPromise;
26
- const pause = ms => new Promise(resolve => setTimeout(resolve, ms));
27
- const service = createHistoryService({ crewRoot: root, runtimeId: 'test', agents: { list: () => [], create: async () => ({}) },
28
- persistence: { supportsRawArtifacts: true, listSnapshots: async () => existsSync(file) ? [{ header: { id: 'session-test', createdAt: 1767225600000 }, revision: 'r1' }] : [], locate: () => ({ kind: 'jsonl', path: file }) },
29
- launch: async id => { operationPromise = pause(200).then(() => runHistoryOperation({ crewRoot: root, id,
30
- acquire: () => ({ ok: true }), release: () => ({ ok: true }), checkFence: () => service.fencedCheck(), assertStopped: () => stopped,
31
- supervisor: { stopOwnedBackend: async () => { stopped = true; await pause(150); return { ok: true }; }, startOwnedBackend: async () => { await pause(150); stopped = false; return { ok: true }; } }, verifyRunning: async () => !stopped,
32
- })); operationPromise.catch(error => errors.push(error.message)); },
33
- });
34
- let historyHandler;
35
- registerHistoryHttp({ register: route => { historyHandler = route.handler; return () => {}; } }, service);
36
- const browser = await chromium.launch({ headless: true, ...(process.env.CREW_BROWSER_EXE ? { executablePath: process.env.CREW_BROWSER_EXE } : {}) });
37
- try {
38
- const page = await browser.newPage({ viewport: { width: 1100, height: 1100 } });
39
- page.on('pageerror', error => errors.push(error.message));
40
- page.on('dialog', dialog => dialog.accept());
41
- await page.route('**/*', async route => {
42
- const request = route.request(); const url = new URL(request.url());
43
- if (url.origin !== 'http://127.0.0.1:41791') return route.abort();
44
- if (url.pathname === '/') return route.fulfill({ contentType: 'text/html', body: '<html><head><meta charset="utf-8"></head><body style="font:14px Arial;background:#f5f6f8"><main id="root" style="padding:24px;max-width:800px;margin:auto;background:white"></main></body></html>' });
45
- if (url.pathname.includes('/history/')) {
46
- if (stopped) return route.abort();
47
- const req = Readable.from([Buffer.from(request.postData() ?? '{}')]);
48
- Object.assign(req, { url: url.pathname, method: request.method(), headers: { host: url.host, origin: url.origin }, socket: { remoteAddress: '127.0.0.1' } });
49
- let status, body;
50
- await historyHandler(req, { writeHead: n => { status = n; }, end: s => { body = s; } });
51
- return route.fulfill({ status, contentType: 'application/json', body });
52
- }
53
- const suffix = url.pathname.split('/').pop(); let body = { ok: true };
54
- if (suffix === 'runtime') body = { ok: true, service: 'dsh-crew-hub', profile: 'dsh-crew', listen_port: 3210 };
55
- if (suffix === 'bridge-status') body = { ok: false };
56
- if (suffix === 'config') body = { ok: true, config: { flash_model_priority: [], pro_model_priority: [], default_timeout_seconds: 1800 } };
57
- if (suffix === 'status') body = { ok: true, status: {} };
58
- return route.fulfill({ contentType: 'application/json', body: JSON.stringify(body) });
59
- });
60
- await page.goto('http://127.0.0.1:41791/'); await page.waitForLoadState('networkidle');
61
- await page.addScriptTag({ content: readFileSync(join(repo, 'node_modules/react/umd/react.development.js'), 'utf8') });
62
- await page.addScriptTag({ content: readFileSync(join(repo, 'node_modules/react-dom/umd/react-dom.development.js'), 'utf8') });
63
- await page.evaluate(() => {
64
- const root = window.ReactDOM.createRoot(document.getElementById('root'));
65
- const ctx = { on: () => () => {}, locale: { getLocale: () => ({ active: 'zh' }) }, slots: { inject: (_n, callback) => callback(), register: (_meta, render) => { root.render(render()); return () => {}; } } };
66
- window.__ModuleLoader__ = { load({ factory }) { const jsx = (tag, props, key) => window.React.createElement(tag, { ...props, ...(key === undefined ? {} : { key }) }); factory(id => {
67
- if (id === 'react') return window.React;
68
- if (id === 'react-dom' || id === 'react-dom/client') return window.ReactDOM;
69
- if (id === 'react/jsx-runtime') return { jsx, jsxs: jsx, Fragment: window.React.Fragment };
70
- throw Error('Unexpected dependency');
71
- }).apply(ctx); } };
72
- });
73
- await page.addScriptTag({ content: readFileSync(join(repo, 'lib/client.js'), 'utf8') });
74
- await page.getByText('工作区与会话清理', { exact: true }).click();
75
- const preview = page.getByRole('button', { name: '预览清理范围', exact: true });
76
- await preview.click();
77
- await page.getByText('1 个工作区 · 1 个会话', { exact: true }).first().waitFor();
78
- const confirm = page.getByRole('button', { name: '确认执行', exact: true });
79
- assert.equal(await confirm.isEnabled(), false);
80
- await page.getByRole('checkbox', { name: '我已确认范围,并同意短暂重启 3210' }).check();
81
- await page.screenshot({ path: join(output, 'preview.png'), fullPage: true });
82
- await confirm.click(); await page.getByRole('button', { name: '恢复', exact: true }).waitFor({ timeout: 15000 }); await operationPromise;
83
- assert.equal(existsSync(file), false);
84
- await page.getByRole('button', { name: '恢复', exact: true }).click();
85
- await page.waitForFunction(() => [...document.querySelectorAll('[role=status]')].some(el => el.textContent === '已完成'));
86
- await operationPromise; assert.equal(readFileSync(file, 'utf8'), 'DISPOSABLE TEST CONVERSATION');
87
- await page.getByRole('combobox', { name: '操作', exact: true }).selectOption('delete');
88
- await page.getByRole('combobox', { name: '时间范围', exact: true }).selectOption('before');
89
- await page.getByLabel('创建时间早于', { exact: true }).fill('2027-01-01T00:00');
90
- await preview.click(); await page.getByRole('checkbox', { name: '我已确认范围,并同意短暂重启 3210' }).check();
91
- assert.equal(await confirm.isEnabled(), false);
92
- await page.getByLabel('删除确认:请输入 DELETE', { exact: true }).fill('DELETE'); await confirm.click();
93
- await page.waitForFunction(() => [...document.querySelectorAll('[role=status]')].some(el => el.textContent === '已完成'));
94
- await operationPromise; assert.equal(existsSync(file), false); assert.deepEqual(service.archives(), []);
95
- await page.screenshot({ path: join(output, 'completed.png'), fullPage: true });
96
- assert.deepEqual(errors, []);
97
- console.log(JSON.stringify({ ok: true, flows: ['archive all', 'restore', 'delete before date', 'confirmation guards', 'reconnect'], screenshots: output }));
98
- } finally { await browser.close(); service.dispose(); rmSync(root, { recursive: true, force: true }); }
@@ -1,26 +0,0 @@
1
- // Live-verify the fixed installer against a fake home: the settings must
2
- // point at the repository itself as the marketplace root (no parent-dir
3
- // assumption), permission rules intact, CLI step skipped in test mode.
4
- import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
5
- import { join } from 'node:path';
6
- import { tmpdir } from 'node:os';
7
- import { installClaudeCode, uninstallClaudeCode } from '../src/install/install.mjs';
8
-
9
- const home = mkdtempSync(join(tmpdir(), 'dsh-crew-fix-test-'));
10
- try {
11
- const r = await installClaudeCode({ home });
12
- console.log('actions:', r.actions.slice(0, 3).join(' | '));
13
- const s = JSON.parse(readFileSync(join(home, '.claude', 'settings.json'), 'utf8'));
14
- const mp = s.extraKnownMarketplaces['dsh-crew'];
15
- const cwd = process.cwd().replaceAll('\\', '/');
16
- const mpPath = String(mp?.source?.path ?? '').replaceAll('\\', '/');
17
- console.log('marketplace path is repo root:', mpPath === cwd, '->', mpPath);
18
- console.log('permission rules:', (s.permissions?.allow ?? []).filter((x) => String(x).includes('dsh-crew')).length);
19
- console.log('cli step skipped in test mode:', r.actions.some((a) => a.includes('test mode')));
20
- if (mpPath !== cwd) throw new Error('marketplace not pointing at repo root');
21
- const u = await uninstallClaudeCode({ home });
22
- console.log('uninstall ok:', u.ok);
23
- } finally {
24
- rmSync(home, { recursive: true, force: true });
25
- }
26
- console.log('INSTALLER FIX VERIFIED');