Skip to content

Commit 3f4ab6f

Browse files
committed
test: pin the published injector, hook, and extension contracts
Fixtures exercise the surfaces third parties rely on: param-name hook signatures across every payload shape and influence channel (mutation, function-return middleware, abort), require-time extension registration against global.$injector including hierarchical commands, and the full IInjector facade surface. Notably they pin that a hook naming an unwrapped payload key (the after-watchAction shape) is skipped as invalid - resolution changes must not resurrect long-dead hooks.
1 parent 3d8b118 commit 3f4ab6f

3 files changed

Lines changed: 506 additions & 0 deletions

File tree

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { assert } from "chai";
2+
import { Yok } from "../../lib/common/yok";
3+
4+
// Pins the externally reachable injector surface: every IInjector member
5+
// (lib/common/definitions/yok.d.ts) plus dispose, subclassability, the
6+
// injector self-registration, and the global assignment. The Yok facade must
7+
// keep all of these behaving through the DI migration.
8+
9+
const FACADE_METHODS = [
10+
"require",
11+
"requirePublic",
12+
"requirePublicClass",
13+
"requireCommand",
14+
"requireKeyCommand",
15+
"resolve",
16+
"resolveCommand",
17+
"resolveKeyCommand",
18+
"register",
19+
"registerCommand",
20+
"registerKeyCommand",
21+
"getRegisteredCommandsNames",
22+
"getRegisteredKeyCommandsNames",
23+
"dynamicCall",
24+
"getDynamicCallData",
25+
"isDefaultCommand",
26+
"isValidHierarchicalCommand",
27+
"getChildrenCommandsNames",
28+
"buildHierarchicalCommand",
29+
"dispose",
30+
];
31+
32+
describe("injector facade surface", () => {
33+
it("exposes every IInjector member", () => {
34+
const inj: any = new Yok();
35+
for (const member of FACADE_METHODS) {
36+
assert.isFunction(inj[member], `missing facade method: ${member}`);
37+
}
38+
assert.instanceOf(inj.dynamicCallRegex, RegExp);
39+
assert.isObject(inj.publicApi);
40+
assert.isObject(inj.publicApi.__modules__);
41+
assert.isBoolean(inj.overrideAlreadyRequiredModule);
42+
});
43+
44+
it("registers itself under 'injector', resolvable with and without the $ prefix", () => {
45+
const inj = new Yok();
46+
assert.strictEqual(inj.resolve("injector"), inj);
47+
assert.strictEqual(inj.resolve("$injector"), inj);
48+
});
49+
50+
it("remains subclassable (the InjectorStub pattern in test/stubs.ts)", () => {
51+
class SubInjector extends Yok {}
52+
const sub = new SubInjector();
53+
sub.register("subclassed", { value: 42 });
54+
assert.equal(sub.resolve("subclassed").value, 42);
55+
assert.strictEqual(sub.resolve("injector"), sub);
56+
});
57+
58+
it("assigns the process-wide global.$injector", () => {
59+
assert.isOk((<any>global).$injector);
60+
for (const member of FACADE_METHODS) {
61+
assert.isFunction(
62+
(<any>global).$injector[member],
63+
`global.$injector missing: ${member}`,
64+
);
65+
}
66+
});
67+
68+
it("calls lowercase/anonymous resolvers as factories instead of new-ing them", () => {
69+
const inj = new Yok();
70+
inj.register("factoryMade", function () {
71+
return { madeByFactory: true };
72+
});
73+
assert.isTrue(inj.resolve("factoryMade").madeByFactory);
74+
});
75+
});

test/compat/legacy-extension.ts

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { assert } from "chai";
2+
import * as fs from "fs";
3+
import * as os from "os";
4+
import * as path from "path";
5+
import { ICliGlobal } from "../../lib/common/definitions/cli-global";
6+
7+
// Pins the published extension contract: an extension is require()d and
8+
// registers its contributions by mutating global.$injector — commands via
9+
// requireCommand/registerCommand (lazy, path-based), services via register.
10+
// extending-cli.md advertises this surface.
11+
12+
const cliGlobal = <ICliGlobal>(<unknown>global);
13+
14+
describe("legacy extension contract", () => {
15+
let extDir: string;
16+
let capture: any;
17+
18+
beforeEach(() => {
19+
extDir = fs.mkdtempSync(path.join(os.tmpdir(), "ns-compat-ext-"));
20+
capture = (<any>global).__extCapture = {};
21+
});
22+
23+
afterEach(() => {
24+
fs.rmSync(extDir, { recursive: true, force: true });
25+
delete (<any>global).__extCapture;
26+
});
27+
28+
it("an unmodified extension contributes services and hierarchical commands via global.$injector", async () => {
29+
const commandPath = path.join(extDir, "meow-command");
30+
fs.writeFileSync(
31+
commandPath + ".js",
32+
`class MeowPurrCommand {
33+
constructor($extCompatService) {
34+
this.extCompatService = $extCompatService;
35+
this.allowedParameters = [];
36+
}
37+
async execute(args) {
38+
global.__extCapture.executedWith = args;
39+
global.__extCapture.service = this.extCompatService;
40+
}
41+
}
42+
global.$injector.registerCommand("meowcompat|purr", MeowPurrCommand);`,
43+
);
44+
fs.writeFileSync(
45+
path.join(extDir, "main.js"),
46+
`global.$injector.register("extCompatService", { name: "ext-compat-service" });
47+
global.$injector.requireCommand("meowcompat|purr", ${JSON.stringify(
48+
commandPath,
49+
)});`,
50+
);
51+
52+
require(path.join(extDir, "main.js"));
53+
54+
// Registration is lazy: the command module must not load until resolved.
55+
assert.isUndefined(capture.executedWith);
56+
57+
// Services registered by the extension resolve under both spellings.
58+
const service = cliGlobal.$injector.resolve("extCompatService");
59+
assert.strictEqual(
60+
cliGlobal.$injector.resolve("$extCompatService"),
61+
service,
62+
);
63+
assert.equal(service.name, "ext-compat-service");
64+
65+
// The command participates in the hierarchical router.
66+
assert.include(
67+
cliGlobal.$injector.getRegisteredCommandsNames(false),
68+
"meowcompat|purr",
69+
);
70+
assert.include(
71+
cliGlobal.$injector.getChildrenCommandsNames("meowcompat"),
72+
"purr",
73+
);
74+
const built = cliGlobal.$injector.buildHierarchicalCommand("meowcompat", [
75+
"purr",
76+
"extra-arg",
77+
]);
78+
assert.equal(built.commandName, "meowcompat|purr");
79+
assert.deepEqual(built.remainingArguments, ["extra-arg"]);
80+
81+
// The leaf command resolves, gets its DI-injected service, and executes.
82+
const command = cliGlobal.$injector.resolveCommand("meowcompat|purr");
83+
assert.isOk(command);
84+
await command.execute(["fluffy"]);
85+
assert.deepEqual(capture.executedWith, ["fluffy"]);
86+
assert.equal(capture.service.name, "ext-compat-service");
87+
88+
// registerCommand on a hierarchical name synthesized a parent dispatcher.
89+
const parent = <any>cliGlobal.$injector.resolveCommand("meowcompat");
90+
assert.isTrue(parent.isHierarchicalCommand);
91+
});
92+
93+
it("claiming an already-required command name throws unless overrideAlreadyRequiredModule is set", () => {
94+
const firstPath = path.join(extDir, "first");
95+
fs.writeFileSync(firstPath + ".js", `module.exports = {};`);
96+
97+
cliGlobal.$injector.requireCommand("conflictcompat", firstPath);
98+
assert.throws(
99+
() => cliGlobal.$injector.requireCommand("conflictcompat", firstPath),
100+
/require'd twice/,
101+
);
102+
103+
cliGlobal.$injector.overrideAlreadyRequiredModule = true;
104+
try {
105+
cliGlobal.$injector.requireCommand("conflictcompat", firstPath);
106+
} finally {
107+
cliGlobal.$injector.overrideAlreadyRequiredModule = false;
108+
}
109+
});
110+
});

0 commit comments

Comments
 (0)