dsh-workspace-mover 2.4.2 → 2.4.3

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.
@@ -38,9 +38,62 @@ test('live lifecycle: explicit stop, request exclusion, failure recovery and unl
38
38
  assert.equal(lifecycleFor(ctx, 'a'), null);
39
39
  });
40
40
 
41
+ test('delete lifecycle: no reload on success, restore after failure only when archive survives', async () => {
42
+ const entries = new Map();
43
+ let reloads = 0;
44
+ let teardown;
45
+ const agents = {
46
+ get: (id) => entries.get(id),
47
+ async resume({ resumeSessionId: id }) {
48
+ const agent = { id, status: 'idle', session: { header: {} }, send() {} };
49
+ entries.set(id, agent);
50
+ return { agent, async dispose() { entries.delete(id); } };
51
+ }
52
+ };
53
+ const ctx = { get: (key) => key === 'agents' ? agents : key === 'sessionController' ? {
54
+ async resolveAgent(id) { reloads++; return agents.resume({ resumeSessionId: id }); }
55
+ } : null };
56
+ installLifecycle(ctx, { agents, effect(fn) { teardown = fn(); } });
57
+ try {
58
+ await agents.resume({ resumeSessionId: 'delete' });
59
+ await withReleasedSession(ctx, 'delete', false, () => 'deleted', { reloadOnSuccess: false });
60
+ assert.equal(entries.has('delete'), false);
61
+ assert.equal(reloads, 0, 'never recreate the deleted conversation');
62
+ await agents.resume({ resumeSessionId: 'rollback' });
63
+ await assert.rejects(withReleasedSession(ctx, 'rollback', false, () => { throw new Error('rollback complete'); },
64
+ { reloadOnSuccess: false, canReload: () => true }), /rollback complete/);
65
+ assert.equal(reloads, 1);
66
+ assert.ok(entries.has('rollback'));
67
+ await assert.rejects(withReleasedSession(ctx, 'rollback', false, () => { throw new Error('archive unavailable'); },
68
+ { reloadOnSuccess: false, canReload: () => false }), /archive unavailable/);
69
+ assert.equal(reloads, 1, 'failed physical rollback must not create a blank session');
70
+ } finally { teardown(); }
71
+ });
72
+
41
73
  const modules = process.env.DSH_TEST_NODE_MODULES;
42
74
  const load = (name) => import(pathToFileURL(join(resolve(modules), '@deepseek-ai', name, 'lib/index.js')).href);
43
75
 
76
+ test('installed LocaleRuntime: bind follows locale changes and disposers unregister dictionaries',
77
+ { skip: !modules }, () => {
78
+ const file = join(resolve(modules), '@deepseek-ai/dsh-client-locale/lib/client.js');
79
+ let exported;
80
+ runInNewContext(readFileSync(file, 'utf8'), {
81
+ window: { __ModuleLoader__: { load({ factory }) { exported = factory(() => ({})); } } },
82
+ navigator: { language: 'zh-CN', languages: ['zh-CN'] }, console
83
+ });
84
+ const locale = new exported.LocaleRuntime({ emit() {} }, undefined, { languages: ['en'], preference: 'en' });
85
+ const stop = locale.register('test-mover', { en: { title: 'Move {n}' }, zh: { title: '移动 {n}' } });
86
+ const t = locale.bind('test-mover');
87
+ assert.equal(t('title', { n: 2 }), 'Move 2');
88
+ let changed = 0;
89
+ const unsubscribe = locale.subscribe(() => changed++);
90
+ locale.setLocale('zh');
91
+ assert.equal(t('title', { n: 2 }), '移动 2');
92
+ assert.equal(changed, 1);
93
+ unsubscribe(); stop();
94
+ assert.equal(t('title'), 'title');
95
+ });
96
+
44
97
  test('installed client Controller: a Provider reference keeps the removed generation alive until released',
45
98
  { skip: !modules, timeout: 10000 }, async () => {
46
99
  const cordis = await load('cordis');
@@ -70,7 +123,11 @@ test('installed client Controller: a Provider reference keeps the removed genera
70
123
  } } }, console, AbortController, AbortSignal, queueMicrotask, setTimeout, clearTimeout });
71
124
  const root = new cordis.Context();
72
125
  try {
73
- const client = new exports.__ClientSessions(root, {});
126
+ let listed = [];
127
+ let listCalls = 0;
128
+ const client = new exports.__ClientSessions(root, { session: {
129
+ async list() { listCalls++; return { ok: true, value: { items: listed } }; }
130
+ } });
74
131
  const id = 'session-retained-view';
75
132
  const summary = { sessionId: id, blank: false, running: false, agentAvailable: true, updatedAt: 1 };
76
133
  client.handleSessionAdded(summary);
@@ -94,6 +151,15 @@ test('installed client Controller: a Provider reference keeps the removed genera
94
151
  assert.notEqual(fresh.binding, previous);
95
152
  assert.equal(fresh.binding.session.getSnapshot().removed, false);
96
153
  fresh.release();
154
+ client.handleSessionRemoved(id);
155
+ await new Promise((resolve) => setTimeout(resolve, 0));
156
+ assert.equal(client.list.getSnapshot().ids.includes(id), false);
157
+ listed = [summary];
158
+ await client.refresh();
159
+ await new Promise((resolve) => setTimeout(resolve, 0));
160
+ assert.equal(listCalls, 1);
161
+ assert.equal(client.list.getSnapshot().ids.includes(id), true,
162
+ 'public refresh restores cold archive metadata without an Agent-created event or restart');
97
163
  } finally {
98
164
  await root.fiber.dispose();
99
165
  }
@@ -174,7 +240,11 @@ test('installed DSH AgentLoop: dispose writer, physical move, resume same sessio
174
240
  off();
175
241
  const again = lifecycleFor(root, id).handle;
176
242
  assert.equal(again.agent.session.header.cwd, a);
177
- await again.dispose();
243
+ const deleted = await mover.deleteSessionToTrash(root, { sessionId: id });
244
+ assert.equal(deleted.deleted, true);
245
+ assert.equal(agents.get(id), undefined, 'deleted Agent is not reloaded');
246
+ assert.equal(root.get('sessions').get(id), undefined, 'real Session writer is released');
247
+ assert.equal(lifecycleFor(root, id), null);
178
248
  } finally {
179
249
  await root.fiber.dispose();
180
250
  if (prior === undefined) delete process.env.DSH_HOME; else process.env.DSH_HOME = prior;
@@ -1,102 +1,102 @@
1
- import test from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import { execFileSync } from 'node:child_process';
4
- import { fileURLToPath } from 'node:url';
5
- import { applyReleaseBadgeToMarkdown, buildReleaseBadgeFiles, RELEASE_BADGE_END, RELEASE_BADGE_START, sanitizeGistPatchFiles, summarizeReleaseAssets } from '../.github/scripts/release-downloads-core.mjs';
6
-
7
- const gistId = 'c14345658550a4a308570acfbaf9d170';
8
-
9
- test('counts prefixed tgz/zip assets and hides the badge at 10 or fewer downloads', () => {
10
- const summary = summarizeReleaseAssets([
11
- {
12
- tag_name: 'v1.4.2',
13
- assets: [
14
- { name: 'dsh-workspace-mover-1.4.2.tgz', download_count: 6 },
15
- { name: 'dsh-workspace-mover-1.4.2.zip', download_count: 4 },
16
- { name: 'source.tar.gz', download_count: 99 }
17
- ]
18
- }
19
- ]);
20
- assert.equal(summary.total, 10);
21
- assert.equal(summary.visible, false);
22
- assert.equal(summary.assets.length, 2);
23
- });
24
-
25
- test('shows a shields.io endpoint badge only after the total exceeds 10', () => {
26
- const summary = summarizeReleaseAssets([
27
- { tag_name: 'v2.0.0', assets: [{ name: 'dsh-workspace-mover-2.0.0.tgz', download_count: 11 }] }
28
- ]);
29
- assert.equal(summary.visible, true);
30
- const files = buildReleaseBadgeFiles({ total: summary.total, assets: summary.assets, updatedAt: '2026-09-10T00:00:00Z', visible: summary.visible });
31
- const badge = JSON.parse(files['wsm-release-downloads.json'].content);
32
- assert.equal(badge.label, 'Release downloads');
33
- assert.equal(badge.message, '11');
34
- assert.equal(badge.color, 'blue');
35
- assert.equal(files['wsm-release-downloads.svg'], null);
36
- });
37
-
38
- test('hidden state deletes gist SVG and shields files so GitHub cannot render a broken alt-text link', () => {
39
- const files = buildReleaseBadgeFiles({ total: 3, assets: [], updatedAt: '2026-09-10T00:00:00Z', visible: false });
40
- assert.equal(files['wsm-release-downloads.json'], null);
41
- assert.equal(files['wsm-release-downloads.svg'], null);
42
- const data = JSON.parse(files['wsm-release-downloads-data.json'].content);
43
- assert.equal(data.total, 3);
44
- assert.equal(data.visible, false);
45
- });
46
-
47
- test('sanitizeGistPatchFiles skips deletes for files that are not on the gist', () => {
48
- const patch = buildReleaseBadgeFiles({ total: 3, assets: [], updatedAt: '2026-09-10T00:00:00Z', visible: false });
49
- const sanitized = sanitizeGistPatchFiles({ 'wsm-release-downloads-data.json': { content: '{}' } }, patch);
50
- assert.deepEqual(Object.keys(sanitized).sort(), ['wsm-release-downloads-data.json']);
51
- assert.equal(sanitized['wsm-release-downloads.json'], undefined);
52
- assert.equal(sanitized['wsm-release-downloads.svg'], undefined);
53
-
54
- const withExisting = sanitizeGistPatchFiles(
55
- {
56
- 'wsm-release-downloads-data.json': { content: '{}' },
57
- 'wsm-release-downloads.json': { content: '{}' },
58
- 'wsm-clones-14d.json': { content: 'x' }
59
- },
60
- { ...patch, 'wsm-clones-14d.json': null }
61
- );
62
- assert.equal(withExisting['wsm-release-downloads.json'], null);
63
- assert.equal(withExisting['wsm-release-downloads.svg'], undefined);
64
- assert.equal(withExisting['wsm-clones-14d.json'], null);
65
- assert.equal(typeof withExisting['wsm-release-downloads-data.json'].content, 'string');
66
- });
67
-
68
- test('sanitizeGistPatchFiles turns empty content into a delete only when the file exists', () => {
69
- const empty = sanitizeGistPatchFiles({}, { 'a.json': { content: '' } });
70
- assert.deepEqual(empty, {});
71
- const present = sanitizeGistPatchFiles({ 'a.json': { content: 'x' } }, { 'a.json': { content: '' } });
72
- assert.deepEqual(present, { 'a.json': null });
73
- });
74
-
75
- test('README keeps an invisible marker below 10 and a shields graphic above 10', () => {
76
- const source = [
77
- '<p>',
78
- ' <img alt="GitHub clones observed" src="https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FPianoPrince%2Fc14345658550a4a308570acfbaf9d170%2Fraw%2Fwsm-clones-total.json" style="height:20px; margin:0 2px;" />',
79
- ' <img alt="Release downloads (shown after 10 downloads)" src="https://gist.githubusercontent.com/PianoPrince/c14345658550a4a308570acfbaf9d170/raw/wsm-release-downloads.svg" style="height:20px; margin:0 2px;" />',
80
- '</p>'
81
- ].join('\n');
82
- const hidden = applyReleaseBadgeToMarkdown(source, { visible: false, gistId });
83
- assert.equal(hidden.includes('wsm-release-downloads.svg'), false);
84
- assert.equal(hidden.includes('Release downloads (shown after 10 downloads)'), false);
85
- assert.equal(hidden.includes('<img alt="Release downloads"'), false);
86
- assert.equal(hidden.includes(RELEASE_BADGE_START), true);
87
- assert.equal(hidden.includes(RELEASE_BADGE_END), true);
88
- const shown = applyReleaseBadgeToMarkdown(hidden, { visible: true, gistId });
89
- assert.equal(shown.includes('img.shields.io/endpoint?url='), true);
90
- assert.equal(shown.includes('wsm-release-downloads.json'), true);
91
- assert.equal(shown.includes('wsm-release-downloads.svg'), false);
92
- assert.equal(shown.includes('<img alt="Release downloads"'), true);
93
- });
94
-
95
- test('extract-changelog accepts both current and historical heading styles', () => {
96
- const script = fileURLToPath(new URL('../.github/scripts/extract-changelog.mjs', import.meta.url));
97
- const current = execFileSync(process.execPath, [script, 'v2.0.1'], { encoding: 'utf8' });
98
- const historical = execFileSync(process.execPath, [script, 'v1.4.2'], { encoding: 'utf8' });
99
- assert.match(current, /Compatible with DeepSeek Harness/);
100
- assert.match(historical, /GitHub Release tags now publish/);
101
- assert.doesNotMatch(historical, /Compatible with DeepSeek Harness/);
102
- });
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { execFileSync } from 'node:child_process';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { applyReleaseBadgeToMarkdown, buildReleaseBadgeFiles, RELEASE_BADGE_END, RELEASE_BADGE_START, sanitizeGistPatchFiles, summarizeReleaseAssets } from '../.github/scripts/release-downloads-core.mjs';
6
+
7
+ const gistId = 'c14345658550a4a308570acfbaf9d170';
8
+
9
+ test('counts prefixed tgz/zip assets and hides the badge at 10 or fewer downloads', () => {
10
+ const summary = summarizeReleaseAssets([
11
+ {
12
+ tag_name: 'v1.4.2',
13
+ assets: [
14
+ { name: 'dsh-workspace-mover-1.4.2.tgz', download_count: 6 },
15
+ { name: 'dsh-workspace-mover-1.4.2.zip', download_count: 4 },
16
+ { name: 'source.tar.gz', download_count: 99 }
17
+ ]
18
+ }
19
+ ]);
20
+ assert.equal(summary.total, 10);
21
+ assert.equal(summary.visible, false);
22
+ assert.equal(summary.assets.length, 2);
23
+ });
24
+
25
+ test('shows a shields.io endpoint badge only after the total exceeds 10', () => {
26
+ const summary = summarizeReleaseAssets([
27
+ { tag_name: 'v2.0.0', assets: [{ name: 'dsh-workspace-mover-2.0.0.tgz', download_count: 11 }] }
28
+ ]);
29
+ assert.equal(summary.visible, true);
30
+ const files = buildReleaseBadgeFiles({ total: summary.total, assets: summary.assets, updatedAt: '2026-09-10T00:00:00Z', visible: summary.visible });
31
+ const badge = JSON.parse(files['wsm-release-downloads.json'].content);
32
+ assert.equal(badge.label, 'Release downloads');
33
+ assert.equal(badge.message, '11');
34
+ assert.equal(badge.color, 'blue');
35
+ assert.equal(files['wsm-release-downloads.svg'], null);
36
+ });
37
+
38
+ test('hidden state deletes gist SVG and shields files so GitHub cannot render a broken alt-text link', () => {
39
+ const files = buildReleaseBadgeFiles({ total: 3, assets: [], updatedAt: '2026-09-10T00:00:00Z', visible: false });
40
+ assert.equal(files['wsm-release-downloads.json'], null);
41
+ assert.equal(files['wsm-release-downloads.svg'], null);
42
+ const data = JSON.parse(files['wsm-release-downloads-data.json'].content);
43
+ assert.equal(data.total, 3);
44
+ assert.equal(data.visible, false);
45
+ });
46
+
47
+ test('sanitizeGistPatchFiles skips deletes for files that are not on the gist', () => {
48
+ const patch = buildReleaseBadgeFiles({ total: 3, assets: [], updatedAt: '2026-09-10T00:00:00Z', visible: false });
49
+ const sanitized = sanitizeGistPatchFiles({ 'wsm-release-downloads-data.json': { content: '{}' } }, patch);
50
+ assert.deepEqual(Object.keys(sanitized).sort(), ['wsm-release-downloads-data.json']);
51
+ assert.equal(sanitized['wsm-release-downloads.json'], undefined);
52
+ assert.equal(sanitized['wsm-release-downloads.svg'], undefined);
53
+
54
+ const withExisting = sanitizeGistPatchFiles(
55
+ {
56
+ 'wsm-release-downloads-data.json': { content: '{}' },
57
+ 'wsm-release-downloads.json': { content: '{}' },
58
+ 'wsm-clones-14d.json': { content: 'x' }
59
+ },
60
+ { ...patch, 'wsm-clones-14d.json': null }
61
+ );
62
+ assert.equal(withExisting['wsm-release-downloads.json'], null);
63
+ assert.equal(withExisting['wsm-release-downloads.svg'], undefined);
64
+ assert.equal(withExisting['wsm-clones-14d.json'], null);
65
+ assert.equal(typeof withExisting['wsm-release-downloads-data.json'].content, 'string');
66
+ });
67
+
68
+ test('sanitizeGistPatchFiles turns empty content into a delete only when the file exists', () => {
69
+ const empty = sanitizeGistPatchFiles({}, { 'a.json': { content: '' } });
70
+ assert.deepEqual(empty, {});
71
+ const present = sanitizeGistPatchFiles({ 'a.json': { content: 'x' } }, { 'a.json': { content: '' } });
72
+ assert.deepEqual(present, { 'a.json': null });
73
+ });
74
+
75
+ test('README keeps an invisible marker below 10 and a shields graphic above 10', () => {
76
+ const source = [
77
+ '<p>',
78
+ ' <img alt="GitHub clones observed" src="https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FPianoPrince%2Fc14345658550a4a308570acfbaf9d170%2Fraw%2Fwsm-clones-total.json" style="height:20px; margin:0 2px;" />',
79
+ ' <img alt="Release downloads (shown after 10 downloads)" src="https://gist.githubusercontent.com/PianoPrince/c14345658550a4a308570acfbaf9d170/raw/wsm-release-downloads.svg" style="height:20px; margin:0 2px;" />',
80
+ '</p>'
81
+ ].join('\n');
82
+ const hidden = applyReleaseBadgeToMarkdown(source, { visible: false, gistId });
83
+ assert.equal(hidden.includes('wsm-release-downloads.svg'), false);
84
+ assert.equal(hidden.includes('Release downloads (shown after 10 downloads)'), false);
85
+ assert.equal(hidden.includes('<img alt="Release downloads"'), false);
86
+ assert.equal(hidden.includes(RELEASE_BADGE_START), true);
87
+ assert.equal(hidden.includes(RELEASE_BADGE_END), true);
88
+ const shown = applyReleaseBadgeToMarkdown(hidden, { visible: true, gistId });
89
+ assert.equal(shown.includes('img.shields.io/endpoint?url='), true);
90
+ assert.equal(shown.includes('wsm-release-downloads.json'), true);
91
+ assert.equal(shown.includes('wsm-release-downloads.svg'), false);
92
+ assert.equal(shown.includes('<img alt="Release downloads"'), true);
93
+ });
94
+
95
+ test('extract-changelog accepts both current and historical heading styles', () => {
96
+ const script = fileURLToPath(new URL('../.github/scripts/extract-changelog.mjs', import.meta.url));
97
+ const current = execFileSync(process.execPath, [script, 'v2.0.1'], { encoding: 'utf8' });
98
+ const historical = execFileSync(process.execPath, [script, 'v1.4.2'], { encoding: 'utf8' });
99
+ assert.match(current, /Compatible with DeepSeek Harness/);
100
+ assert.match(historical, /GitHub Release tags now publish/);
101
+ assert.doesNotMatch(historical, /Compatible with DeepSeek Harness/);
102
+ });
@@ -1,80 +1,80 @@
1
- import test from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import { buildBadgeFiles, buildState, buildStructuredDaily, mergeHistory, normalizeClonesResponse, normalizePathsResponse, normalizeReferrersResponse, normalizeViewsResponse, parseHistory } from '../.github/scripts/traffic-badge-core.mjs';
4
-
5
- const snapshot = {
6
- count: 12,
7
- uniques: 7,
8
- clones: [
9
- { timestamp: '2026-09-08T00:00:00Z', count: 5, uniques: 3 },
10
- { timestamp: '2026-09-09T00:00:00Z', count: 7, uniques: 4 }
11
- ]
12
- };
13
-
14
- test('traffic response uses GitHub clones[] rows and rejects the old days[] shape', () => {
15
- const normalized = normalizeClonesResponse(snapshot);
16
- assert.deepEqual(normalized.days, [
17
- { date: '2026-09-08', count: 5, uniques: 3 },
18
- { date: '2026-09-09', count: 7, uniques: 4 }
19
- ]);
20
- assert.throws(() => normalizeClonesResponse({ count: 12, uniques: 7, days: snapshot.clones }), /no clones array/);
21
- });
22
-
23
- test('traffic normalizes views, referrers, and popular paths', () => {
24
- assert.deepEqual(normalizeViewsResponse({ count: 9, uniques: 4, views: [{ timestamp: '2026-09-09T00:00:00Z', count: 9, uniques: 4 }] }).days, [
25
- { date: '2026-09-09', count: 9, uniques: 4 }
26
- ]);
27
- assert.deepEqual(normalizeReferrersResponse([{ referrer: 'github.com', count: 3, uniques: 2 }]), [
28
- { referrer: 'github.com', count: 3, uniques: 2 }
29
- ]);
30
- assert.deepEqual(normalizePathsResponse([{ path: '/README.md', title: 'README', count: 5, uniques: 3 }]), [
31
- { path: '/README.md', title: 'README', count: 5, uniques: 3 }
32
- ]);
33
- });
34
-
35
- test('history upsert replaces corrected dates without double counting', () => {
36
- const old = parseHistory('{"date":"2026-09-08","count":4,"uniques":2}\n');
37
- const merged = mergeHistory(old, [
38
- { date: '2026-09-08', count: 5, uniques: 3 },
39
- { date: '2026-09-09', count: 7, uniques: 4 }
40
- ]);
41
- assert.deepEqual(merged, [
42
- { date: '2026-09-08', count: 5, uniques: 3 },
43
- { date: '2026-09-09', count: 7, uniques: 4 }
44
- ]);
45
- const state = buildState({}, merged, normalizeClonesResponse(snapshot), '2026-09-10T00:00:00Z');
46
- assert.equal(state.cumulativeClones, 12);
47
- assert.equal(state.observedSince, '2026-09-08');
48
- });
49
-
50
- test('schema migration preserves the old value as a legacy estimate only', () => {
51
- const state = buildState({ cumulative: 193 }, [], { ...normalizeClonesResponse(snapshot), days: [] }, '2026-09-10T00:00:00Z');
52
- assert.equal(state.schemaVersion, 2);
53
- assert.equal(state.cumulativeClones, 0);
54
- assert.equal(state.legacyEstimate, 193);
55
- assert.equal(state.lastWindow.count, 12);
56
- });
57
-
58
- test('badge exposes cumulative clones and cumulative uniques since the observation start', () => {
59
- const state = buildState({}, parseHistory('{"date":"2026-09-08","count":5,"uniques":3}\n{"date":"2026-09-09","count":7,"uniques":4}\n'), normalizeClonesResponse(snapshot), '2026-09-10T00:00:00Z');
60
- assert.equal(state.cumulativeUniques, 7);
61
- const files = buildBadgeFiles(state);
62
- const total = JSON.parse(files['wsm-clones-total.json'].content);
63
- assert.equal(total.label, 'GitHub clones observed');
64
- assert.equal(total.message, '12 since 2026-09-08 · 7 unique');
65
- assert.equal(files['wsm-clones-14d.json'], undefined);
66
- });
67
-
68
- test('structured daily backfills JSONL-only early days without clobbering views', () => {
69
- const existing = [{ date: '2026-09-09', clones: { date: '2026-09-09', count: 7, uniques: 4 }, views: { date: '2026-09-09', count: 2, uniques: 1 } }];
70
- const historyRows = parseHistory('{"date":"2026-09-07","count":1,"uniques":1}\n{"date":"2026-09-08","count":5,"uniques":3}\n{"date":"2026-09-09","count":99,"uniques":99}\n');
71
- const cloneDays = [{ date: '2026-09-09', count: 7, uniques: 4 }];
72
- const viewDays = [{ date: '2026-09-09', count: 2, uniques: 1 }];
73
- const daily = buildStructuredDaily(existing, historyRows, cloneDays, viewDays);
74
- assert.deepEqual(daily.map((row) => row.date), ['2026-09-07', '2026-09-08', '2026-09-09']);
75
- assert.deepEqual(daily[0].clones, { date: '2026-09-07', count: 1, uniques: 1 });
76
- assert.equal(daily[0].views, undefined, 'JSONL 回填行没有 views 数据');
77
- // JSONL 与快照冲突时快照优先:09-09 保持 7/4,且 views 不丢
78
- assert.deepEqual(daily[2].clones, { date: '2026-09-09', count: 7, uniques: 4 });
79
- assert.deepEqual(daily[2].views, { date: '2026-09-09', count: 2, uniques: 1 });
80
- });
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { buildBadgeFiles, buildState, buildStructuredDaily, mergeHistory, normalizeClonesResponse, normalizePathsResponse, normalizeReferrersResponse, normalizeViewsResponse, parseHistory } from '../.github/scripts/traffic-badge-core.mjs';
4
+
5
+ const snapshot = {
6
+ count: 12,
7
+ uniques: 7,
8
+ clones: [
9
+ { timestamp: '2026-09-08T00:00:00Z', count: 5, uniques: 3 },
10
+ { timestamp: '2026-09-09T00:00:00Z', count: 7, uniques: 4 }
11
+ ]
12
+ };
13
+
14
+ test('traffic response uses GitHub clones[] rows and rejects the old days[] shape', () => {
15
+ const normalized = normalizeClonesResponse(snapshot);
16
+ assert.deepEqual(normalized.days, [
17
+ { date: '2026-09-08', count: 5, uniques: 3 },
18
+ { date: '2026-09-09', count: 7, uniques: 4 }
19
+ ]);
20
+ assert.throws(() => normalizeClonesResponse({ count: 12, uniques: 7, days: snapshot.clones }), /no clones array/);
21
+ });
22
+
23
+ test('traffic normalizes views, referrers, and popular paths', () => {
24
+ assert.deepEqual(normalizeViewsResponse({ count: 9, uniques: 4, views: [{ timestamp: '2026-09-09T00:00:00Z', count: 9, uniques: 4 }] }).days, [
25
+ { date: '2026-09-09', count: 9, uniques: 4 }
26
+ ]);
27
+ assert.deepEqual(normalizeReferrersResponse([{ referrer: 'github.com', count: 3, uniques: 2 }]), [
28
+ { referrer: 'github.com', count: 3, uniques: 2 }
29
+ ]);
30
+ assert.deepEqual(normalizePathsResponse([{ path: '/README.md', title: 'README', count: 5, uniques: 3 }]), [
31
+ { path: '/README.md', title: 'README', count: 5, uniques: 3 }
32
+ ]);
33
+ });
34
+
35
+ test('history upsert replaces corrected dates without double counting', () => {
36
+ const old = parseHistory('{"date":"2026-09-08","count":4,"uniques":2}\n');
37
+ const merged = mergeHistory(old, [
38
+ { date: '2026-09-08', count: 5, uniques: 3 },
39
+ { date: '2026-09-09', count: 7, uniques: 4 }
40
+ ]);
41
+ assert.deepEqual(merged, [
42
+ { date: '2026-09-08', count: 5, uniques: 3 },
43
+ { date: '2026-09-09', count: 7, uniques: 4 }
44
+ ]);
45
+ const state = buildState({}, merged, normalizeClonesResponse(snapshot), '2026-09-10T00:00:00Z');
46
+ assert.equal(state.cumulativeClones, 12);
47
+ assert.equal(state.observedSince, '2026-09-08');
48
+ });
49
+
50
+ test('schema migration preserves the old value as a legacy estimate only', () => {
51
+ const state = buildState({ cumulative: 193 }, [], { ...normalizeClonesResponse(snapshot), days: [] }, '2026-09-10T00:00:00Z');
52
+ assert.equal(state.schemaVersion, 2);
53
+ assert.equal(state.cumulativeClones, 0);
54
+ assert.equal(state.legacyEstimate, 193);
55
+ assert.equal(state.lastWindow.count, 12);
56
+ });
57
+
58
+ test('badge exposes cumulative clones and cumulative uniques since the observation start', () => {
59
+ const state = buildState({}, parseHistory('{"date":"2026-09-08","count":5,"uniques":3}\n{"date":"2026-09-09","count":7,"uniques":4}\n'), normalizeClonesResponse(snapshot), '2026-09-10T00:00:00Z');
60
+ assert.equal(state.cumulativeUniques, 7);
61
+ const files = buildBadgeFiles(state);
62
+ const total = JSON.parse(files['wsm-clones-total.json'].content);
63
+ assert.equal(total.label, 'GitHub clones observed');
64
+ assert.equal(total.message, '12 since 2026-09-08 · 7 unique');
65
+ assert.equal(files['wsm-clones-14d.json'], undefined);
66
+ });
67
+
68
+ test('structured daily backfills JSONL-only early days without clobbering views', () => {
69
+ const existing = [{ date: '2026-09-09', clones: { date: '2026-09-09', count: 7, uniques: 4 }, views: { date: '2026-09-09', count: 2, uniques: 1 } }];
70
+ const historyRows = parseHistory('{"date":"2026-09-07","count":1,"uniques":1}\n{"date":"2026-09-08","count":5,"uniques":3}\n{"date":"2026-09-09","count":99,"uniques":99}\n');
71
+ const cloneDays = [{ date: '2026-09-09', count: 7, uniques: 4 }];
72
+ const viewDays = [{ date: '2026-09-09', count: 2, uniques: 1 }];
73
+ const daily = buildStructuredDaily(existing, historyRows, cloneDays, viewDays);
74
+ assert.deepEqual(daily.map((row) => row.date), ['2026-09-07', '2026-09-08', '2026-09-09']);
75
+ assert.deepEqual(daily[0].clones, { date: '2026-09-07', count: 1, uniques: 1 });
76
+ assert.equal(daily[0].views, undefined, 'JSONL 回填行没有 views 数据');
77
+ // JSONL 与快照冲突时快照优先:09-09 保持 7/4,且 views 不丢
78
+ assert.deepEqual(daily[2].clones, { date: '2026-09-09', count: 7, uniques: 4 });
79
+ assert.deepEqual(daily[2].views, { date: '2026-09-09', count: 2, uniques: 1 });
80
+ });