@@ -33,6 +33,7 @@ import * as WorkspacePaths from "../workspace/WorkspacePaths.ts";
3333import { assetFileResponse } from "../http.ts" ;
3434import { ASSET_ROUTE_PREFIX , issueAssetUrl , readMcpAppHtml , resolveAsset } from "./AssetAccess.ts" ;
3535import * as NativeAppIconResolver from "./NativeAppIconResolver.ts" ;
36+ import * as WorkspacePreviewRevisions from "./WorkspacePreviewRevisions.ts" ;
3637import { symlinksSupported } from "@t3tools/shared/testing/symlinks" ;
3738import { fakeSourceControlDriverInstance } from "@t3tools/source-control-testing/fakeSourceControlDriver" ;
3839import * as BuiltInDrivers from "../sourceControl/builtInDrivers.ts" ;
@@ -118,6 +119,7 @@ const layerTest = Layer.mergeAll(
118119 Layer . provide ( T3ProjectFileLoader . layer ) ,
119120 ) ,
120121 NativeAppIconResolver . layer . pipe ( Layer . provide ( layerConfig ) ) ,
122+ WorkspacePreviewRevisions . layer ,
121123 ServerSecretStore . layer . pipe ( Layer . provide ( layerConfig ) ) ,
122124) . pipe ( Layer . provideMerge ( NodeServices . layer ) ) ;
123125
@@ -846,6 +848,72 @@ describe("AssetAccess", () => {
846848 } ) . pipe ( Effect . provide ( layerTest ) ) ,
847849 ) ;
848850
851+ it . effect . each ( [ "workspace-file" , "draft-workspace-file" ] as const ) (
852+ "refreshes %s HTML when loaded local assets change" ,
853+ ( kind ) =>
854+ Effect . gen ( function * ( ) {
855+ const fs = yield * FileSystem . FileSystem ;
856+ const path = yield * Path . Path ;
857+ const root = yield * fs . makeTempDirectoryScoped ( { prefix : "t3-preview-dependencies-" } ) ;
858+ const htmlPath = path . join ( root , "report.html" ) ;
859+ const cssPath = path . join ( root , "styles.css" ) ;
860+ const scriptPath = path . join ( root , "app.js" ) ;
861+ const unrelatedPath = path . join ( root , "unrelated.css" ) ;
862+ yield * fs . writeFileString (
863+ htmlPath ,
864+ '<link rel="stylesheet" href="styles.css"><script src="app.js"></script>' ,
865+ ) ;
866+ yield * fs . writeFileString ( cssPath , "body{color:red}" ) ;
867+ yield * fs . writeFileString ( scriptPath , "window.answer=1" ) ;
868+ yield * fs . writeFileString ( unrelatedPath , "unrelated" ) ;
869+ const input = {
870+ resource :
871+ kind === "draft-workspace-file"
872+ ? { _tag : kind , cwd : root , path : "report.html" }
873+ : { _tag : kind , threadId : ThreadId . make ( "thread-1" ) , path : htmlPath } ,
874+ workspaceRoot : root ,
875+ } ;
876+ const original = yield * issueAssetUrl ( input ) ;
877+ const suffix = original . relativeUrl . slice ( `${ ASSET_ROUTE_PREFIX } /` . length ) ;
878+ const token = suffix . slice ( 0 , suffix . indexOf ( "/" ) ) ;
879+ for ( const name of [ "report.html" , "styles.css" , "app.js" ] ) {
880+ expect ( yield * resolveAsset ( token , name ) ) . not . toBeNull ( ) ;
881+ }
882+ expect ( yield * resolveAsset ( token , "missing.css" ) ) . toBeNull ( ) ;
883+ yield * fs . writeFileString ( unrelatedPath , "edited unrelated file" ) ;
884+ const unchanged = yield * issueAssetUrl ( input ) ;
885+ expect ( unchanged . fileRevision ) . toBe ( original . fileRevision ) ;
886+
887+ yield * fs . writeFileString ( cssPath , "body{color:tan}" ) ;
888+ yield * Effect . promise ( ( ) => NodeFSP . utimes ( cssPath , 2_000 , 2_000 ) ) ;
889+ const cssEdited = yield * issueAssetUrl ( input ) ;
890+ expect ( cssEdited . fileRevision ) . not . toBe ( unchanged . fileRevision ) ;
891+ expect ( ( yield * issueAssetUrl ( input ) ) . fileRevision ) . toBe ( cssEdited . fileRevision ) ;
892+
893+ yield * fs . writeFileString ( scriptPath , "window.answer=2" ) ;
894+ yield * Effect . promise ( ( ) => NodeFSP . utimes ( scriptPath , 3_000 , 3_000 ) ) ;
895+ expect ( yield * resolveAsset ( token , "app.js" ) ) . not . toBeNull ( ) ;
896+ const jsEdited = yield * issueAssetUrl ( input ) ;
897+ expect ( jsEdited . fileRevision ) . not . toBe ( cssEdited . fileRevision ) ;
898+
899+ const replacement = path . join ( root , "replacement.js" ) ;
900+ yield * fs . writeFileString ( replacement , "window.answer=2" ) ;
901+ yield * Effect . promise ( ( ) => NodeFSP . utimes ( replacement , 3_000 , 3_000 ) ) ;
902+ yield * fs . rename ( replacement , scriptPath ) ;
903+ const replaced = yield * issueAssetUrl ( input ) ;
904+ expect ( replaced . fileRevision ) . not . toBe ( jsEdited . fileRevision ) ;
905+
906+ yield * fs . remove ( cssPath ) ;
907+ const deleted = yield * issueAssetUrl ( input ) ;
908+ expect ( deleted . fileRevision ) . not . toBe ( replaced . fileRevision ) ;
909+ expect ( ( yield * issueAssetUrl ( input ) ) . fileRevision ) . toBe ( deleted . fileRevision ) ;
910+
911+ yield * fs . writeFileString ( path . join ( root , "missing.css" ) , "body{color:red}" ) ;
912+ const created = yield * issueAssetUrl ( input ) ;
913+ expect ( created . fileRevision ) . not . toBe ( deleted . fileRevision ) ;
914+ } ) . pipe ( Effect . provide ( layerTest ) ) ,
915+ ) ;
916+
849917 it . effect ( "keeps in-place edits readable but requires a new URL after atomic replacement" , ( ) =>
850918 Effect . gen ( function * ( ) {
851919 const fs = yield * FileSystem . FileSystem ;
0 commit comments