test(cmdpal): clean up Phase 6 ownership

Restore the inherited manifest test surface, pin generated form IDs, and clarify what the copied gallery fixture covers.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 62a92164-1ef1-4c78-9678-26fd596e2ff8
This commit is contained in:
Michael Jolley
2026-08-28 13:44:09 -05:00
parent d67ec9cd28
commit f7560e6a6d
3 changed files with 25 additions and 160 deletions

View File

@@ -287,10 +287,8 @@ public class ExtensionGalleryServiceTests
[TestMethod] [TestMethod]
public async Task FetchExtensionsAsync_ParsesDocumentedJsonRpcGalleryExample() public async Task FetchExtensionsAsync_ParsesDocumentedJsonRpcGalleryExample()
{ {
// This JSON is the gallery feed example published in // This copy validates compatibility with the documented JSON shape, but it does not read
// doc/json-rpc-spec/04-manifest-packaging.md. It is fed through the production parser so // the docs file. If that example changes, this fixture must be updated by hand.
// the documentation and the real gallery model stay in sync. The example omits iconUrl
// so the test stays offline.
var feedDirectory = CreateTempDirectory("feed"); var feedDirectory = CreateTempDirectory("feed");
var cacheDirectory = CreateTempDirectory("cache"); var cacheDirectory = CreateTempDirectory("cache");

View File

@@ -866,162 +866,6 @@ public class JSExtensionManifestTests
} }
} }
[TestMethod]
public void TryParse_RelativeIcon_ResolvesToContainedAbsolutePath()
{
CreateEntryPoint("dist/index.js");
CreateEntryPoint("assets/icon.png");
const string Json = """
{
"name": "relative-icon",
"main": "dist/index.js",
"cmdpal": { "icon": "assets/icon.png" }
}
""";
var result = JSExtensionManifest.TryParse(Json, _testDirectory);
Assert.IsTrue(result.IsValid, result.FailureReason);
var expected = Path.GetFullPath(Path.Combine(_testDirectory, "assets", "icon.png"));
Assert.AreEqual(expected, result.Manifest!.IconPath);
}
[TestMethod]
public void TryParse_RootDirectory_IsResolvedToPackageRoot()
{
CreateEntryPoint("dist/index.js");
const string Json = """
{
"name": "root-directory",
"main": "dist/index.js",
"cmdpal": {}
}
""";
var result = JSExtensionManifest.TryParse(Json, _testDirectory);
Assert.IsTrue(result.IsValid, result.FailureReason);
var expected = Path.TrimEndingDirectorySeparator(Path.GetFullPath(_testDirectory));
Assert.AreEqual(expected, result.Manifest!.RootDirectory);
}
[TestMethod]
public void TryParse_RelativeIcon_ThatEscapesPackage_ResolvesToEmpty()
{
CreateEntryPoint("dist/index.js");
const string Json = """
{
"name": "escaping-icon",
"main": "dist/index.js",
"cmdpal": { "icon": "../outside-icon.png" }
}
""";
var result = JSExtensionManifest.TryParse(Json, _testDirectory);
Assert.IsTrue(result.IsValid, result.FailureReason);
Assert.AreEqual(string.Empty, result.Manifest!.IconPath);
}
[TestMethod]
public void TryParse_RelativeIcon_ThatDoesNotExist_ResolvesToEmpty()
{
CreateEntryPoint("dist/index.js");
const string Json = """
{
"name": "missing-icon",
"main": "dist/index.js",
"cmdpal": { "icon": "assets/missing.png" }
}
""";
var result = JSExtensionManifest.TryParse(Json, _testDirectory);
Assert.IsTrue(result.IsValid, result.FailureReason);
Assert.AreEqual(string.Empty, result.Manifest!.IconPath);
}
[TestMethod]
public void TryParse_GlyphIcon_IsPreservedUnchanged()
{
CreateEntryPoint("dist/index.js");
const string Json = """
{
"name": "glyph-icon",
"main": "dist/index.js",
"cmdpal": { "icon": "\uE700" }
}
""";
var result = JSExtensionManifest.TryParse(Json, _testDirectory);
Assert.IsTrue(result.IsValid, result.FailureReason);
Assert.AreEqual("\uE700", result.Manifest!.IconPath);
}
[TestMethod]
public void TryParse_UriIcon_IsPreservedUnchanged()
{
CreateEntryPoint("dist/index.js");
const string Json = """
{
"name": "uri-icon",
"main": "dist/index.js",
"cmdpal": { "icon": "https://example.com/icon.png" }
}
""";
var result = JSExtensionManifest.TryParse(Json, _testDirectory);
Assert.IsTrue(result.IsValid, result.FailureReason);
Assert.AreEqual("https://example.com/icon.png", result.Manifest!.IconPath);
}
[TestMethod]
public void TryParse_IconThroughJunction_ResolvesToEmpty()
{
// An icon whose lexical path stays inside the package but traverses a junction that
// redirects outside the package must resolve to empty rather than load the outside file.
CreateEntryPoint("dist/index.js");
var outsideDirectory = Path.Combine(Path.GetTempPath(), $"JSExtensionIconJunctionTarget_{Guid.NewGuid():N}");
Directory.CreateDirectory(outsideDirectory);
File.WriteAllText(Path.Combine(outsideDirectory, "icon.png"), "// icon bytes");
var junctionPath = Path.Combine(_testDirectory, "linked-assets");
if (!TryCreateJunction(junctionPath, outsideDirectory))
{
Directory.Delete(outsideDirectory, recursive: true);
Assert.Inconclusive("A directory junction could not be created in this environment.");
return;
}
try
{
const string Json = """
{
"name": "junction-icon",
"main": "dist/index.js",
"cmdpal": { "icon": "linked-assets/icon.png" }
}
""";
var result = JSExtensionManifest.TryParse(Json, _testDirectory);
Assert.IsTrue(result.IsValid, result.FailureReason);
Assert.AreEqual(string.Empty, result.Manifest!.IconPath);
}
finally
{
if (Directory.Exists(junctionPath))
{
Directory.Delete(junctionPath, recursive: false);
}
Directory.Delete(outsideDirectory, recursive: true);
}
}
private static bool TryCreateJunction(string junctionPath, string targetPath) private static bool TryCreateJunction(string junctionPath, string targetPath)
{ {
try try

View File

@@ -218,6 +218,29 @@ describe('form identity and routing', () => {
expect(responseFor(sent, 4)?.result).toEqual({ kind: 2 }); expect(responseFor(sent, 4)?.result).toEqual({ kind: 2 });
}); });
it('assigns a deterministic formId when the author omits one', async () => {
const page: IContentPage = {
id: 'page',
name: 'Page',
title: 'Page',
getContent(): Content[] {
return [formContent(undefined, () => ({ kind: 'goHome' }))];
},
};
const { runtime, sent } = createHarness();
runtime.setProvider(providerWith(page));
await runtime.handleRequest({
jsonrpc: JSONRPC_VERSION,
id: 1,
method: 'contentPage/getContent',
params: { pageId: 'page' },
});
const content = responseFor(sent, 1)?.result as Array<Record<string, unknown>>;
expect(content[0]?.formId).toBe('form-0');
});
it('keeps routing a nested form by its stable id after the tree grows', async () => { it('keeps routing a nested form by its stable id after the tree grows', async () => {
// Mirrors the comments sample: submitting a reply mutates the tree, and the // Mirrors the comments sample: submitting a reply mutates the tree, and the
// next serialization must still route the same stable formId back to its // next serialization must still route the same stable formId back to its