|
| 1 | +import * as assert from "assert"; |
| 2 | +import { VSCode } from "@types"; |
| 3 | +import RelatedItemLinkProvider from "../../../editor/relatedItemLinkProvider"; |
| 4 | +import RelatedItemDiagnosticProvider from "../../../editor/relatedItemDiagnosticProvider"; |
| 5 | + |
| 6 | +const KEY = "shared-key"; |
| 7 | +const BU = "retrieve/cred/bu"; |
| 8 | + |
| 9 | +/** Builds a single-line relation document in either asset context. */ |
| 10 | +function documentFor(inside: boolean, bu = BU, text = `{"r__asset_key":"${KEY}"}`): VSCode.TextDocument { |
| 11 | + const path = `/workspace/${bu}/${inside ? "asset/message/source" : "journey"}/source.json`; |
| 12 | + return { |
| 13 | + uri: VSCode.Uri.file(path), |
| 14 | + getText: () => text, |
| 15 | + positionAt: (offset: number) => new VSCode.Position(0, offset) |
| 16 | + } as VSCode.TextDocument; |
| 17 | +} |
| 18 | + |
| 19 | +/** Lists the exact metadata candidates in their required lookup order. */ |
| 20 | +function candidates(inside: boolean, bu = BU): string[] { |
| 21 | + const subtype = inside ? "template" : "message"; |
| 22 | + return [ |
| 23 | + `${bu}/asset/${subtype}/${KEY}/${KEY}.asset-${subtype}-meta.json`, |
| 24 | + `${bu}/asset/mobile/${KEY}.asset-mobile-meta.json`, |
| 25 | + `${bu}/asset/webstudio/${KEY}.asset-webstudio-meta.json`, |
| 26 | + `${bu}/asset/webstudio/${KEY}/${KEY}.asset-webstudio-meta.json` |
| 27 | + ]; |
| 28 | +} |
| 29 | + |
| 30 | +suite("Related item providers – asset metadata resolution", () => { |
| 31 | + const originalFindFiles = VSCode.workspace.findFiles; |
| 32 | + let files: Set<string>; |
| 33 | + let calls: string[]; |
| 34 | + let links: RelatedItemLinkProvider; |
| 35 | + let diagnostics: RelatedItemDiagnosticProvider; |
| 36 | + |
| 37 | + setup(() => { |
| 38 | + files = new Set(); |
| 39 | + calls = []; |
| 40 | + links = new RelatedItemLinkProvider(); |
| 41 | + diagnostics = new RelatedItemDiagnosticProvider(); |
| 42 | + VSCode.workspace.findFiles = async (pattern, _exclude, maxResults) => { |
| 43 | + assert.strictEqual(typeof pattern, "string"); |
| 44 | + const path = pattern as string; |
| 45 | + calls.push(path); |
| 46 | + const matches = path.endsWith("/**") |
| 47 | + ? [...files].filter(file => file.startsWith(path.slice(0, -2))) |
| 48 | + : [...files].filter(file => file === path); |
| 49 | + return matches.slice(0, maxResults).map(file => VSCode.Uri.file(`/workspace/${file}`)); |
| 50 | + }; |
| 51 | + }); |
| 52 | + |
| 53 | + teardown(() => { |
| 54 | + VSCode.workspace.findFiles = originalFindFiles; |
| 55 | + diagnostics.getDiagnosticCollection().dispose(); |
| 56 | + }); |
| 57 | + |
| 58 | + for (const inside of [false, true]) { |
| 59 | + const context = inside ? "inside asset" : "outside asset"; |
| 60 | + for (const index of [0, 1, 2, 3]) { |
| 61 | + test(`${context}: candidate ${index} resolves and stops before lower-priority matches`, async () => { |
| 62 | + const paths = candidates(inside); |
| 63 | + // All later candidates exist too, proving precedence rather than mere discovery. |
| 64 | + files = new Set(paths.slice(index)); |
| 65 | + const document = documentFor(inside); |
| 66 | + const result = await links.provideDocumentLinks(document); |
| 67 | + assert.strictEqual(result.length, 1); |
| 68 | + assert.strictEqual(result[0].target?.path, `/workspace/${paths[index]}`); |
| 69 | + const start = document.getText().indexOf(KEY); |
| 70 | + assert.strictEqual(result[0].range.start.character, start); |
| 71 | + assert.strictEqual(result[0].range.end.character, start + KEY.length); |
| 72 | + assert.deepStrictEqual(calls, paths.slice(0, index + 1)); |
| 73 | + calls = []; |
| 74 | + await diagnostics.validateDocument(document); |
| 75 | + assert.deepStrictEqual(diagnostics.getDiagnosticCollection().get(document.uri), []); |
| 76 | + assert.deepStrictEqual(calls, [`${BU}/asset/**`, ...paths.slice(0, index + 1)]); |
| 77 | + }); |
| 78 | + } |
| 79 | + |
| 80 | + test(`${context}: missing key warns after every candidate, without searching other subtypes`, async () => { |
| 81 | + const paths = candidates(inside); |
| 82 | + // The opposite legacy context and source fragments must not satisfy this reference. |
| 83 | + files = new Set([candidates(!inside)[0], `${BU}/asset/webstudio/${KEY}/${KEY}.html`]); |
| 84 | + const document = documentFor(inside); |
| 85 | + assert.deepStrictEqual(await links.provideDocumentLinks(document), []); |
| 86 | + assert.deepStrictEqual(calls, paths); |
| 87 | + calls = []; |
| 88 | + await diagnostics.validateDocument(document); |
| 89 | + const result = diagnostics.getDiagnosticCollection().get(document.uri)!; |
| 90 | + assert.strictEqual(result.length, 1); |
| 91 | + assert.strictEqual(result[0].severity, VSCode.DiagnosticSeverity.Warning); |
| 92 | + assert.ok(result[0].message.includes("was not found on the BU")); |
| 93 | + assert.strictEqual((result[0].code as { value: string }).value, "warnOnMissingJsonRelation"); |
| 94 | + assert.deepStrictEqual(calls, [`${BU}/asset/**`, ...paths]); |
| 95 | + }); |
| 96 | + } |
| 97 | + |
| 98 | + test("missing folder warns without attempting key resolution", async () => { |
| 99 | + const document = documentFor(false); |
| 100 | + assert.deepStrictEqual(await links.provideDocumentLinks(document), []); |
| 101 | + assert.deepStrictEqual(calls, candidates(false)); |
| 102 | + calls = []; |
| 103 | + await diagnostics.validateDocument(document); |
| 104 | + const result = diagnostics.getDiagnosticCollection().get(document.uri)!; |
| 105 | + assert.strictEqual(result.length, 1); |
| 106 | + assert.strictEqual(result[0].severity, VSCode.DiagnosticSeverity.Warning); |
| 107 | + assert.ok(result[0].message.includes("type folder has not been retrieved")); |
| 108 | + assert.deepStrictEqual(calls, [`${BU}/asset/**`]); |
| 109 | + }); |
| 110 | + |
| 111 | + test("typeFilter rejects the relation before any filesystem lookup", async () => { |
| 112 | + diagnostics.getDiagnosticCollection().dispose(); |
| 113 | + diagnostics = new RelatedItemDiagnosticProvider((type, project) => { |
| 114 | + assert.strictEqual(type, "asset"); |
| 115 | + assert.strictEqual(project, "/workspace"); |
| 116 | + return false; |
| 117 | + }); |
| 118 | + const document = documentFor(false); |
| 119 | + await diagnostics.validateDocument(document); |
| 120 | + assert.deepStrictEqual(diagnostics.getDiagnosticCollection().get(document.uri), []); |
| 121 | + assert.deepStrictEqual(calls, []); |
| 122 | + }); |
| 123 | + |
| 124 | + for (const present of [false, true]) { |
| 125 | + test(`sequential lookups reuse ${present ? "positive" : "negative"} caches`, async () => { |
| 126 | + const target = candidates(false)[3]; |
| 127 | + files.add(`${BU}/asset/other.json`); |
| 128 | + if (present) files.add(target); |
| 129 | + const document = documentFor(false); |
| 130 | + const firstLinks = await links.provideDocumentLinks(document); |
| 131 | + await diagnostics.validateDocument(document); |
| 132 | + const firstDiagnostics = diagnostics.getDiagnosticCollection().get(document.uri); |
| 133 | + assert.strictEqual(firstLinks.length, present ? 1 : 0); |
| 134 | + assert.strictEqual(firstDiagnostics?.length, present ? 0 : 1); |
| 135 | + assert.strictEqual(calls.length, 9); |
| 136 | + calls = []; |
| 137 | + // Change the backing files; cache behavior intentionally remains unchanged. |
| 138 | + if (present) files.delete(target); |
| 139 | + else files.add(target); |
| 140 | + assert.deepStrictEqual(await links.provideDocumentLinks(document), firstLinks); |
| 141 | + await diagnostics.validateDocument(document); |
| 142 | + assert.deepStrictEqual(diagnostics.getDiagnosticCollection().get(document.uri), firstDiagnostics); |
| 143 | + assert.deepStrictEqual(calls, []); |
| 144 | + }); |
| 145 | + } |
| 146 | + |
| 147 | + test("same key caches remain separated by BU and asset context", async () => { |
| 148 | + const otherBu = "retrieve/cred/other-bu"; |
| 149 | + files = new Set([candidates(false)[0], candidates(true)[0], `${otherBu}/asset/other.json`]); |
| 150 | + for (const [inside, bu, expected] of [ |
| 151 | + [false, BU, candidates(false)[0]], |
| 152 | + [true, BU, candidates(true)[0]], |
| 153 | + [false, otherBu, undefined] |
| 154 | + ] as const) { |
| 155 | + calls = []; |
| 156 | + const document = documentFor(inside, bu); |
| 157 | + const result = await links.provideDocumentLinks(document); |
| 158 | + assert.strictEqual(result.length, expected ? 1 : 0); |
| 159 | + assert.strictEqual(result[0]?.target?.path, expected ? `/workspace/${expected}` : undefined); |
| 160 | + await diagnostics.validateDocument(document); |
| 161 | + assert.strictEqual(diagnostics.getDiagnosticCollection().get(document.uri)?.length, expected ? 0 : 1); |
| 162 | + assert.ok(calls.length > 0); |
| 163 | + assert.ok(calls.every(path => path.startsWith(`${bu}/`))); |
| 164 | + } |
| 165 | + }); |
| 166 | + |
| 167 | + test("automation asset relations retain forward and reverse parsing", async () => { |
| 168 | + files.add(candidates(false)[1]); |
| 169 | + for (const text of [`{"r__type":"asset","r__key":"${KEY}"}`, `{"r__key":"${KEY}","r__type":"asset"}`]) { |
| 170 | + const document = documentFor(false, BU, text); |
| 171 | + const result = await links.provideDocumentLinks(document); |
| 172 | + assert.strictEqual(result.length, 1); |
| 173 | + assert.strictEqual(result[0].target?.path, `/workspace/${candidates(false)[1]}`); |
| 174 | + assert.strictEqual(result[0].range.start.character, text.indexOf(KEY)); |
| 175 | + await diagnostics.validateDocument(document); |
| 176 | + assert.deepStrictEqual(diagnostics.getDiagnosticCollection().get(document.uri), []); |
| 177 | + } |
| 178 | + }); |
| 179 | +}); |
0 commit comments