@@ -20,7 +20,7 @@ import { SidekickOwnerError } from "../src/repl/sidekick/owner.ts";
2020import { readFile , writeFile , readdir , stat } from "node:fs/promises" ;
2121import { join } from "node:path" ;
2222import { scoped } from "effection" ;
23- import { ensureDir , writeTextFile , rm , exists } from "@effectionx/fs" ;
23+ import { ensureDir , writeTextFile , readTextFile , rm , exists } from "@effectionx/fs" ;
2424import { API , cwd , useHostFiles , Permissions } from "@executablemd/runtime" ;
2525import { useTemporary } from "./fixtures/repl/issue-883/sidekick-harness.ts" ;
2626import { readGuideChapter } from "../src/repl/sidekick/guide.ts" ;
@@ -4035,7 +4035,198 @@ describe("REPL syntax matches ordinary entry execution (#1053)", () => {
40354035 ) . toContain ( name ) ;
40364036 }
40374037
4038+ it ( "ordinary assertions execute the advertised source (#1066)" , function * ( ) {
4039+ const directory = yield * useTemporary ( "xmd-1066-success-" ) ;
4040+ yield * API . Env . around ( {
4041+ * cwd ( ) {
4042+ return directory ;
4043+ } ,
4044+ } ) ;
4045+ const held = yield * useHarness ( ) ;
4046+ const outcomes = yield * exchange (
4047+ held ,
4048+ submission ( `<Let as="actual" value={2 + 2} />
4049+ <AssertEquals actual={actual} expected={4} />
4050+ <Output>Check passed.</Output>` ) ,
4051+ ) ;
4052+ const outcome = outcomes . at ( - 1 ) ;
4053+ if ( outcome ?. outcome . status === "err" ) {
4054+ throw new Error ( outcome . outcome . error . message ) ;
4055+ }
4056+ expect ( outcome ?. outcome . status ) . toBe ( "ok" ) ;
4057+ expect ( held . session . model . entries . at ( - 1 ) ?. terminal ) . toMatchObject ( {
4058+ status : "ok" ,
4059+ output : "Check passed." ,
4060+ } ) ;
4061+ } ) ;
4062+
4063+ it ( "ordinary assertion failure joins cleanup before the next entry (#1066)" , function * ( ) {
4064+ const directory = yield * useTemporary ( "xmd-1066-failure-" ) ;
4065+ yield * API . Env . around ( {
4066+ * cwd ( ) {
4067+ return directory ;
4068+ } ,
4069+ } ) ;
4070+ yield * useHostFiles ( ) ;
4071+ let writes = 0 ;
4072+ yield * API . Files . around ( {
4073+ * writeTextFile ( args , next ) {
4074+ writes += 1 ;
4075+ return yield * next ( ...args ) ;
4076+ } ,
4077+ } ) ;
4078+ const cleaning = withResolvers < void > ( ) ;
4079+ const release = withResolvers < void > ( ) ;
4080+ let installations = 0 ;
4081+ let cleanups = 0 ;
4082+ const held = yield * useHarness ( undefined , {
4083+ entryInstallation : {
4084+ * install ( ) {
4085+ installations += 1 ;
4086+ if ( installations === 1 ) {
4087+ yield * ensure ( function * ( ) {
4088+ cleaning . resolve ( ) ;
4089+ yield * release . operation ;
4090+ cleanups += 1 ;
4091+ } ) ;
4092+ }
4093+ } ,
4094+ } ,
4095+ } ) ;
4096+ yield * ensure ( ( ) => release . resolve ( ) ) ;
4097+ const submitted = yield * held . session . submit (
4098+ '<AssertEquals actual="actual-1066" expected="expected-1066" />\n<File path="sentinel.txt">unwritten</File>' ,
4099+ ) ;
4100+ if ( ! submitted . ok ) {
4101+ throw submitted . error ;
4102+ }
4103+ let joined = false ;
4104+ const joining = yield * spawn ( function * ( ) {
4105+ const result = yield * submitted . value . join ( ) ;
4106+ joined = true ;
4107+ return result ;
4108+ } ) ;
4109+ yield * cleaning . operation ;
4110+ expect ( held . session . admits ( ) . ok ) . toBe ( false ) ;
4111+ const successor = yield * spawn ( ( ) => held . session . submit ( "Too early." ) ) ;
4112+ expect ( ( yield * successor ) . ok ) . toBe ( false ) ;
4113+ expect ( joined ) . toBe ( false ) ;
4114+ expect ( cleanups ) . toBe ( 0 ) ;
4115+ expect ( writes ) . toBe ( 0 ) ;
4116+ expect ( yield * exists ( join ( directory , "sentinel.txt" ) ) ) . toBe ( false ) ;
4117+ release . resolve ( ) ;
4118+ const failed = yield * joining ;
4119+ expect ( failed . ok ) . toBe ( false ) ;
4120+ expect ( cleanups ) . toBe ( 1 ) ;
4121+ const terminal = held . session . model . entries [ 0 ] ?. terminal ;
4122+ expect ( terminal ?. status ) . toBe ( "err" ) ;
4123+ expect ( terminal ?. message ) . toContain ( "actual-1066" ) ;
4124+ expect ( terminal ?. message ) . toContain ( "expected-1066" ) ;
4125+ const next = yield * held . session . submit (
4126+ "<Assert expr={true} /><Output>Next entry passed.</Output>" ,
4127+ ) ;
4128+ if ( ! next . ok ) {
4129+ throw next . error ;
4130+ }
4131+ expect ( next . value . entry ) . not . toBe ( submitted . value . entry ) ;
4132+ expect ( ( yield * next . value . join ( ) ) . ok ) . toBe ( true ) ;
4133+ expect ( held . session . model . entries [ 1 ] ?. terminal ) . toMatchObject ( {
4134+ status : "ok" ,
4135+ output : "Next entry passed." ,
4136+ } ) ;
4137+ expect ( yield * submitted . value . join ( ) ) . toEqual ( failed ) ;
4138+ expect ( held . session . model . entries [ 0 ] ?. terminal ) . toEqual ( terminal ) ;
4139+ expect ( cleanups ) . toBe ( 1 ) ;
4140+ expect ( installations ) . toBe ( 2 ) ;
4141+ expect ( held . fake . prompts ) . toHaveLength ( 0 ) ;
4142+ const journal = yield * readTextFile ( held . execution . stream . path ) ;
4143+ expect ( journal ) . not . toContain ( "test_result" ) ;
4144+ expect ( journal ) . not . toContain ( "testing_boundary" ) ;
4145+ } ) ;
4146+
4147+ it ( "ordinary assertions retain forms, validation and explicit test boundaries (#1066)" , function * ( ) {
4148+ const directory = yield * useTemporary ( "xmd-1066-forms-" ) ;
4149+ yield * API . Env . around ( {
4150+ * cwd ( ) {
4151+ return directory ;
4152+ } ,
4153+ } ) ;
4154+ yield * useHostFiles ( ) ;
4155+ let writes = 0 ;
4156+ yield * API . Files . around ( {
4157+ * writeTextFile ( args , next ) {
4158+ writes += 1 ;
4159+ return yield * next ( ...args ) ;
4160+ } ,
4161+ } ) ;
4162+ const held = yield * useHarness ( ) ;
4163+ const forms = yield * held . session
4164+ . submit ( `<AssertThrows message={/cannot read/} as="caught"><File path="missing-1066.txt" /></AssertThrows>
4165+ <AssertEquals actual={caught.source} expected="File" />
4166+ <AssertStringIncludes actual={caught.message} expected="cannot read" />
4167+ <AssertEquals actual="paired">paired</AssertEquals>
4168+ <Output>Forms passed.</Output>` ) ;
4169+ if ( ! forms . ok ) {
4170+ throw forms . error ;
4171+ }
4172+ const formResult = yield * forms . value . join ( ) ;
4173+ if ( ! formResult . ok ) {
4174+ throw formResult . error ;
4175+ }
4176+ expect ( held . session . model . entries . at ( - 1 ) ?. terminal ?. output ) . toBe ( "Forms passed." ) ;
4177+ const invalid = yield * held . session . submit (
4178+ '<Assert expr={true} bogus="invalid" /><File path="invalid.txt">unwritten</File>' ,
4179+ ) ;
4180+ expect ( invalid . ok ) . toBe ( false ) ;
4181+ if ( ! invalid . ok ) {
4182+ expect ( invalid . error . message ) . toContain ( "bogus" ) ;
4183+ }
4184+ expect ( held . session . model . entries ) . toHaveLength ( 1 ) ;
4185+ const missing = yield * held . session . submit (
4186+ '<AssertThrows><File path="missing-1066.txt" /></AssertThrows>' ,
4187+ ) ;
4188+ if ( ! missing . ok ) {
4189+ throw missing . error ;
4190+ }
4191+ const missingResult = yield * missing . value . join ( ) ;
4192+ expect ( missingResult . ok ) . toBe ( false ) ;
4193+ if ( ! missingResult . ok ) {
4194+ expect ( missingResult . error . message ) . toContain ( 'requires a "message"' ) ;
4195+ }
4196+ const bare = yield * held . session . submit (
4197+ '<Test name="inactive"><File path="test-body.txt">unwritten</File></Test>' ,
4198+ ) ;
4199+ if ( ! bare . ok ) {
4200+ throw bare . error ;
4201+ }
4202+ expect ( ( yield * bare . value . join ( ) ) . ok ) . toBe ( false ) ;
4203+ expect ( held . session . model . entries . at ( - 1 ) ?. terminal ?. message ) . toContain (
4204+ "requires a testing package" ,
4205+ ) ;
4206+ const agent = yield * held . session . submit (
4207+ '<TestAgent><File path="agent-body.txt">unwritten</File></TestAgent>' ,
4208+ ) ;
4209+ expect ( agent . ok ) . toBe ( false ) ;
4210+ if ( ! agent . ok ) {
4211+ expect ( agent . error . message ) . toContain ( "Cannot resolve component: TestAgent" ) ;
4212+ }
4213+ expect ( writes ) . toBe ( 0 ) ;
4214+ for ( const path of [ "invalid.txt" , "test-body.txt" , "agent-body.txt" ] ) {
4215+ expect ( yield * exists ( join ( directory , path ) ) ) . toBe ( false ) ;
4216+ }
4217+ expect ( held . fake . prompts ) . toHaveLength ( 0 ) ;
4218+ const journal = yield * readTextFile ( held . execution . stream . path ) ;
4219+ expect ( journal ) . not . toContain ( "test_result" ) ;
4220+ expect ( journal ) . not . toContain ( "testing_boundary" ) ;
4221+ } ) ;
4222+
40384223 it ( "A1-A4/A7: discovery, admission, permissions and Markdown tests agree" , function * ( ) {
4224+ const directory = yield * useTemporary ( "xmd-1066-discovery-" ) ;
4225+ yield * API . Env . around ( {
4226+ * cwd ( ) {
4227+ return directory ;
4228+ } ,
4229+ } ) ;
40394230 yield * useHostFiles ( ) ;
40404231 let writesRequested = 0 ;
40414232 yield * Permissions . around ( {
@@ -4054,41 +4245,37 @@ describe("REPL syntax matches ordinary entry execution (#1053)", () => {
40544245<REPL.Syntax names={["Json"]} />
40554246<REPL.Syntax names={["Assert"]} />
40564247<REPL.Syntax names={["AssertEquals"]} />
4057- <REPL.Syntax names={["File"]} />` ,
4248+ <REPL.Syntax names={["File"]} />
4249+ <REPL.Syntax names={["Testing"]} />` ,
40584250 ) ;
40594251 const listing = documented ( discovery ) ;
40604252 expect ( listing ) . toContain ( "### `<Json>`" ) ;
40614253 expect ( listing ) . toContain ( "### `<File>`" ) ;
4062- for ( const name of [ "Assert" , "AssertEquals" , "Verbose" , "WebForm" ] ) {
4254+ for ( const name of [ "Assert" , "AssertEquals" ] ) {
4255+ expect ( listing ) . toContain ( `### \`<${ name } >\`` ) ;
4256+ }
4257+ for ( const name of [ "Testing" , "Verbose" , "WebForm" ] ) {
40634258 expect ( listing ) . not . toContain ( `### \`<${ name } >\`` ) ;
40644259 }
40654260 expect ( documented ( discovery , 1 ) ) . toContain ( "**Available in this evaluation:** yes" ) ;
40664261 expect ( documented ( discovery , 4 ) ) . toContain ( "**Available in this evaluation:** yes" ) ;
40674262 for ( const index of [ 2 , 3 ] ) {
4068- const text = documented ( discovery , index ) ;
4069- expect ( text ) . toContain ( "**Available in this evaluation:** no" ) ;
4070- expect ( text ) . toContain ( "ordinary entry" ) ;
4071- expect ( text ) . toContain ( "Markdown test" ) ;
4072- expect ( text ) . toContain ( "<Assert expr={true} />" ) ;
4073- expect ( text ) . toContain ( '<AssertEquals actual="yes" expected="yes" />' ) ;
4074- expect ( text ) . toContain ( '<REPL.Test path="components/assertions.test.md" />' ) ;
4263+ expect ( documented ( discovery , index ) ) . toContain ( "**Available in this evaluation:** yes" ) ;
40754264 }
4265+ const reference = documented ( discovery , 5 ) ;
4266+ expect ( reference ) . toContain ( "**Available in this evaluation:** no" ) ;
4267+ expect ( reference ) . toContain ( "Markdown test" ) ;
4268+ expect ( reference ) . toContain ( '<REPL.Test path="components/assertions.test.md" />' ) ;
40764269 expect ( held . session . model . entries ) . toHaveLength ( 0 ) ;
40774270 expect ( yield * exists ( join ( held . workspace , "sentinel.txt" ) ) ) . toBe ( false ) ;
4078- for ( const source of [
4079- "<Assert expr={true} />" ,
4080- '<AssertEquals actual="yes" expected="yes" />' ,
4081- "<NotAComponent1053 />" ,
4082- ] ) {
4083- const outcomes = yield * exchange (
4084- held ,
4085- submission ( `${ source } <File path="sentinel.txt">unwritten</File>` ) ,
4086- ) ;
4087- refused ( outcomes , "Cannot resolve component" ) ;
4088- expect ( held . session . model . entries ) . toHaveLength ( 0 ) ;
4089- expect ( yield * exists ( join ( held . workspace , "sentinel.txt" ) ) ) . toBe ( false ) ;
4090- expect ( writesRequested ) . toBe ( 0 ) ;
4091- }
4271+ const outcomes = yield * exchange (
4272+ held ,
4273+ submission ( '<NotAComponent1053 /><File path="sentinel.txt">unwritten</File>' ) ,
4274+ ) ;
4275+ refused ( outcomes , "Cannot resolve component" ) ;
4276+ expect ( held . session . model . entries ) . toHaveLength ( 0 ) ;
4277+ expect ( yield * exists ( join ( held . workspace , "sentinel.txt" ) ) ) . toBe ( false ) ;
4278+ expect ( writesRequested ) . toBe ( 0 ) ;
40924279 const unknown = yield * exchange (
40934280 held ,
40944281 `<REPL.Syntax names={["NotAComponent1053"]} />
@@ -4111,7 +4298,7 @@ describe("REPL syntax matches ordinary entry execution (#1053)", () => {
41114298 expect ( yield * exists ( join ( held . workspace , "refused.txt" ) ) ) . toBe ( false ) ;
41124299 expect ( writesRequested ) . toBeGreaterThan ( 0 ) ;
41134300 yield * ensureDir ( join ( held . workspace , "components" ) ) ;
4114- const examples = documented ( discovery , 2 ) . split ( "```xmd\n" ) ;
4301+ const examples = reference . split ( "```xmd\n" ) ;
41154302 const testSource = examples [ 1 ] ?. split ( "\n```" ) [ 0 ] ;
41164303 const testRequest = examples [ 2 ] ?. split ( "\n```" ) [ 0 ] ;
41174304 if ( testSource === undefined || testRequest === undefined ) {
@@ -4129,7 +4316,7 @@ describe("REPL syntax matches ordinary entry execution (#1053)", () => {
41294316 it ( "A5: fresh asks follow session component creation, edits and removal" , function * ( ) {
41304317 const held = yield * useHarness ( ) ;
41314318 const before = yield * exchange ( held , '<REPL.Syntax names={["Assert"]} />' ) ;
4132- expect ( documented ( before ) ) . toContain ( "**Available in this evaluation:** no " ) ;
4319+ expect ( documented ( before ) ) . toContain ( "**Available in this evaluation:** yes " ) ;
41334320 yield * ensureDir ( join ( held . workspace , "components" ) ) ;
41344321 const path = join ( held . workspace , "components/Assert.md" ) ;
41354322 for ( const version of [ "first" , "second" ] ) {
@@ -4161,14 +4348,13 @@ Session body ${version}.`,
41614348 `<REPL.Syntax />
41624349<REPL.Syntax names={["Assert"]} />` ,
41634350 ) ;
4164- expect ( documented ( removed ) ) . not . toContain ( "### `<Assert>`" ) ;
4165- expect ( documented ( removed , 1 ) ) . toContain ( "**Available in this evaluation:** no" ) ;
4166- refused (
4167- yield * exchange ( held , submission ( "<Assert expr={true} />" ) ) ,
4168- "Cannot resolve component: Assert" ,
4169- ) ;
4170- expect ( held . session . model . entries ) . toHaveLength ( 2 ) ;
4171- expect ( documented ( before ) ) . toContain ( "**Available in this evaluation:** no" ) ;
4351+ expect ( documented ( removed ) ) . toContain ( "### `<Assert>`" ) ;
4352+ expect ( documented ( removed , 1 ) ) . toContain ( "**Available in this evaluation:** yes" ) ;
4353+ expect ( documented ( removed , 1 ) ) . not . toContain ( "Session assertion" ) ;
4354+ yield * exchange ( held , submission ( "<Assert expr={true} />" ) ) ;
4355+ expect ( held . session . model . entries . at ( - 1 ) ?. terminal ?. status ) . toBe ( "ok" ) ;
4356+ expect ( held . session . model . entries ) . toHaveLength ( 3 ) ;
4357+ expect ( documented ( before ) ) . toContain ( "**Available in this evaluation:** yes" ) ;
41724358 const retained = yield * records ( held . execution ) ;
41734359 expect ( retained ) . toContainEqual ( before [ 0 ] ) ;
41744360 } ) ;
0 commit comments