-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathconsole-resolve.test.ts
More file actions
222 lines (196 loc) · 8.71 KB
/
Copy pathconsole-resolve.test.ts
File metadata and controls
222 lines (196 loc) · 8.71 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
/**
* resolveConsolePath() hardening — stale out-of-workspace installs.
*
* Node module resolution from the consumer cwd climbs `node_modules`
* directories all the way up the filesystem. A stray
* `~/node_modules/@objectstack/console` left behind by an old npm
* experiment used to win over the version-locked bundle and serve a
* stale Console (browser-side OBJUI-001 "Unknown component type").
* These tests pin the major-version guard that skips such candidates.
*/
import { describe, it, expect } from 'vitest';
import fs from 'fs';
import os from 'os';
import path from 'path';
import { createRequire } from 'module';
import {
resolveConsolePath,
isConsoleVersionCompatible,
warnOnConsoleShaDrift,
} from '../src/utils/console.js';
// resolveConsolePath() also discovers the real, version-locked workspace
// @objectstack/console via the CLI's own module location (createRequire on
// import.meta.url), and that package shares the CLI's fixed-group version.
// Pin the guard's reference version to the live major — read from the CLI's
// own package.json — so these fixtures don't go stale and warn-mismatch on
// every major bump (the 9.x -> 10.0.0 release broke them once already).
const CLI_MAJOR = Number.parseInt(
createRequire(import.meta.url)('../package.json').version,
10,
);
const CLI_VERSION = `${CLI_MAJOR}.2.0`;
/** Same major as the CLI — a healthy, version-locked install. */
const MATCHING_VERSION = `${CLI_MAJOR}.0.0`;
/** A different major — the stale, out-of-workspace install shape. */
const STALE_VERSION = `${CLI_MAJOR - 1}.8.0`;
function writeConsolePackage(
dir: string,
{ name = '@objectstack/console', version, withDist = true }: {
name?: string;
version: string;
withDist?: boolean;
},
): string {
const pkgDir = path.join(dir, 'node_modules', '@objectstack', 'console');
fs.mkdirSync(pkgDir, { recursive: true });
fs.writeFileSync(path.join(pkgDir, 'package.json'), JSON.stringify({ name, version }));
if (withDist) {
fs.mkdirSync(path.join(pkgDir, 'dist'), { recursive: true });
fs.writeFileSync(path.join(pkgDir, 'dist', 'index.html'), '<html></html>');
}
return pkgDir;
}
/** Fresh sandbox: <tmp>/home/project is the cwd, <tmp>/home simulates $HOME. */
function makeSandbox(): { home: string; project: string } {
// realpath: node's require.resolve returns symlink-resolved paths, and
// macOS tmpdir lives behind the /var -> /private/var symlink.
const root = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'os-console-resolve-')));
const home = path.join(root, 'home');
const project = path.join(home, 'project');
fs.mkdirSync(project, { recursive: true });
fs.writeFileSync(
path.join(project, 'package.json'),
JSON.stringify({ name: 'consumer-app', version: '1.0.0' }),
);
return { home, project };
}
describe('isConsoleVersionCompatible', () => {
it('accepts the same major', () => {
expect(isConsoleVersionCompatible('9.2.0', '9.2.0')).toBe(true);
expect(isConsoleVersionCompatible('9.0.1', '9.5.0')).toBe(true);
expect(isConsoleVersionCompatible('9.3.0-beta.1', '9.2.0')).toBe(true);
});
it('rejects a different major', () => {
expect(isConsoleVersionCompatible('7.8.0', '9.2.0')).toBe(false);
expect(isConsoleVersionCompatible('10.0.0', '9.2.0')).toBe(false);
});
it('rejects missing or malformed versions', () => {
expect(isConsoleVersionCompatible(undefined, '9.2.0')).toBe(false);
expect(isConsoleVersionCompatible('', '9.2.0')).toBe(false);
expect(isConsoleVersionCompatible('not-a-version', '9.2.0')).toBe(false);
});
});
describe('resolveConsolePath version guard', () => {
it('skips a stale major-mismatched install climbed to outside the project, with a warning', () => {
const { home, project } = makeSandbox();
// The incident shape: ~/node_modules/@objectstack/console@7.8.0 with a
// built dist, reachable from the project cwd by climbing node_modules.
const stale = writeConsolePackage(home, { version: STALE_VERSION });
const warnings: string[] = [];
const result = resolveConsolePath({
cwd: project,
cliVersion: CLI_VERSION,
warn: (m) => warnings.push(m),
});
expect(result).not.toBe(stale);
expect(warnings.some((m) => m.includes(STALE_VERSION) && m.includes(stale))).toBe(true);
});
it('accepts a same-major install climbed to from the project cwd', () => {
const { home, project } = makeSandbox();
const ok = writeConsolePackage(home, { version: MATCHING_VERSION });
const warnings: string[] = [];
const result = resolveConsolePath({
cwd: project,
cliVersion: CLI_VERSION,
warn: (m) => warnings.push(m),
});
expect(result).toBe(ok);
expect(warnings).toEqual([]);
});
it('prefers a matching local install over a stale parent-directory one', () => {
const { home, project } = makeSandbox();
writeConsolePackage(home, { version: STALE_VERSION });
const local = writeConsolePackage(project, { version: MATCHING_VERSION });
const result = resolveConsolePath({
cwd: project,
cliVersion: CLI_VERSION,
warn: () => {},
});
expect(result).toBe(local);
});
it('skips an install whose package.json carries no version', () => {
const { home, project } = makeSandbox();
const unversionedDir = path.join(home, 'node_modules', '@objectstack', 'console');
fs.mkdirSync(path.join(unversionedDir, 'dist'), { recursive: true });
fs.writeFileSync(
path.join(unversionedDir, 'package.json'),
JSON.stringify({ name: '@objectstack/console' }),
);
fs.writeFileSync(path.join(unversionedDir, 'dist', 'index.html'), '<html></html>');
const warnings: string[] = [];
const result = resolveConsolePath({
cwd: project,
cliVersion: CLI_VERSION,
warn: (m) => warnings.push(m),
});
expect(result).not.toBe(unversionedDir);
expect(warnings.some((m) => m.includes('unknown'))).toBe(true);
});
});
describe('warnOnConsoleShaDrift', () => {
const SHA_A = '2b86379384f0f6e99d9a5bb81d73017fd6f99cef';
const SHA_B = '69d6b94419bcaa11223344556677889900aabbcc';
/**
* Lay out a monorepo-shaped tree: <root>/.objectui-sha is the pin, and
* <root>/packages/console/dist is the vendored, optionally-stamped build.
* Returns the console package dir (what resolveConsolePath would hand back).
*/
function makePinnedTree(
pin: string,
stamp: string | null,
): { root: string; consoleDir: string } {
const root = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'os-console-sha-')));
fs.writeFileSync(path.join(root, '.objectui-sha'), `${pin}\n`);
const consoleDir = path.join(root, 'packages', 'console');
fs.mkdirSync(path.join(consoleDir, 'dist'), { recursive: true });
fs.writeFileSync(path.join(consoleDir, 'dist', 'index.html'), '<html></html>');
if (stamp !== null) {
fs.writeFileSync(path.join(consoleDir, 'dist', '.objectui-sha'), `${stamp}\n`);
}
return { root, consoleDir };
}
it('warns when the dist stamp differs from the pin (the pull-without-rebuild case)', () => {
const { consoleDir } = makePinnedTree(SHA_A, SHA_B);
const warnings: string[] = [];
warnOnConsoleShaDrift(consoleDir, (m) => warnings.push(m));
expect(warnings).toHaveLength(1);
expect(warnings[0]).toContain(SHA_A.slice(0, 12));
expect(warnings[0]).toContain(SHA_B.slice(0, 12));
expect(warnings[0]).toContain('objectui:build');
});
it('is silent when the dist stamp matches the pin', () => {
const { consoleDir } = makePinnedTree(SHA_A, SHA_A);
const warnings: string[] = [];
warnOnConsoleShaDrift(consoleDir, (m) => warnings.push(m));
expect(warnings).toEqual([]);
});
it('is silent for an unstamped dist (pre-guard build / sibling-repo fallback)', () => {
const { consoleDir } = makePinnedTree(SHA_A, null);
const warnings: string[] = [];
warnOnConsoleShaDrift(consoleDir, (m) => warnings.push(m));
expect(warnings).toEqual([]);
});
it('is silent when no pin exists up-tree (published install)', () => {
// A console package with a stamped dist but NO .objectui-sha anywhere
// above it — the shape of a published npm install.
const root = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'os-console-sha-pub-')));
const consoleDir = path.join(root, 'node_modules', '@objectstack', 'console');
fs.mkdirSync(path.join(consoleDir, 'dist'), { recursive: true });
fs.writeFileSync(path.join(consoleDir, 'dist', 'index.html'), '<html></html>');
fs.writeFileSync(path.join(consoleDir, 'dist', '.objectui-sha'), `${SHA_B}\n`);
const warnings: string[] = [];
warnOnConsoleShaDrift(consoleDir, (m) => warnings.push(m));
expect(warnings).toEqual([]);
});
});